Saturday, January 9, 2016

The differences between TextBox and Label in WPF

You can use two elements to show text in GUI, so-called "Label". These are
  • Label: <Label>Text</Label>
  • TextBlock: <TextBlock>Text</TextBlock>
Both produce nearly the same result. But there are differences. TextBlock inherits from FrameworkElement whereas Label inherits from ContentControl --> Control --> FrameworkElement.

This difference lead to all differences that these elements have.

One consequence can directly shown if we rewrite the code from above.
  • Label: <Label Content="Text" />
  • TextBlock: <TextBlock Text="Text" />
TextBlock has no Content, just Text. So TextBlock is restricted to strings, while Label can also have other types as content like images.
<Label>
    <Image Source="text.png" />
</Label>
One other frequently mentioned consequence from the different roots of Label and TextBlock is that Label supports Access Keys (Mnemonics), while TextBlock don't.
<Label Content="_Text"
       Target="{Binding ElementName=InputField}" />
<TextBox x:Name="InputField" />

Furthermore a Label is grayed out, if its IsEnabled property is false, whereas TextBlock isn't. You can also use the Template property for a custom control template or the ContentTemplate property to apply a DataTemplate to its content.

Summarized you can say the difference is that with a Label you can do more than with a TextBlock. You can do whatever a ContentControl can do, while a TextBlock can only do what a FrameworkElement can do. But the huge advantage of the TextBlock is that it is much more lightweight.

So use TextBlock, whenever you just need text. If text is insufficient, use Label.


Thursday, December 17, 2015

Setting the Background of a WPF TextBox depending on Validation in a Template/Style using TemplateBinding 2


I showed how to change the Background of a TextBox, if some own ValidationRule failed in the post Setting the Background of a WPF TextBox depending on Validation in a Template/Style using TemplateBinding. But the solution has a minor issue.

If the VisualState is ReadOnly and the Trigger Validation.HasError is called the Background changes as expected. But if the Trigger is not any longer true, the Background will not set back to the ReadOnly style, again.

To fix this issue the VisualState of ReadOnly is removed.

                                <VisualState x:Name="ReadOnly" />

And a Trigger for the IsReadOnly property is defined. This Trigger must be placed above the Validation.HasError Trigger. Otherwise the TextBox would have always the ReadOnly color and never the HasError color.

        <Style.Triggers>
            <Trigger Property="IsReadOnly"
                     Value="True">
                <Setter Property="Background"
                        Value="LightBlue" />
            </Trigger>
            <Trigger Property="Validation.HasError"
                     Value="True">
                <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                                        Path=(Validation.Errors).CurrentItem.ErrorContent}" />
                <Setter Property="Background"
                        Value="Orange" />
                <Setter Property="BorderThickness"
                        Value="2" />
                <Setter Property="BorderBrush"
                        Value="Red" />
            </Trigger>
        </Style.Triggers>
 

Tuesday, November 3, 2015

Reading and Writing Values from INI Files with C#

INI files is often used by legacy software. But sometimes also by not so legacy software. So sometimes it is needed to access INI files from .NET/C#, but there is no class or method in .NET/C# that can read or write INI files. But there are some functions in kernel32.dll that can be used by Platform Invoke (PInvoke). These functions (GetPrivateProfileString, GetPrivateProfileSection and WritePrivateProfileString) are provided for compatibility, nevertheless they can be used.

You need to add a using that allows using PInvoke.

using System.Runtime.InteropServices;

Then you can declare the methods that you want to access.

[DllImport("kernel32", CharSet = CharSet.Unicode)]
private static extern int GetPrivateProfileString(string section, string key,
    string defaultValue, StringBuilder value, int size, string filePath);
 
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern int GetPrivateProfileString(string section, string key, string defaultValue,
    [InOutchar[] value, int size, string filePath);
 
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern int GetPrivateProfileSection(string section, IntPtr keyValue,
    int size, string filePath);
 
[DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
[returnMarshalAs(UnmanagedType.Bool)]
private static extern bool WritePrivateProfileString(string section, string key,
    string value, string filePath);

Now we can use these methods to access an INI file. Therefor we have defined a size into that the results should fit. You can extend or reduce the size as needed.
public static int capacity = 512;
You can read values with the following code.
public static string ReadValue(string section, string key, string filePath, string defaultValue = "")
{
    var value = new StringBuilder(capacity);
    GetPrivateProfileString(section, key, defaultValue, value, value.Capacity, filePath);
    return value.ToString();
}
 
public static string[] ReadSections(string filePath)
{
    // first line will not recognize if ini file is saved in UTF-8 with BOM
    while (true)
    {
        char[] chars = new char[capacity];
        int size = GetPrivateProfileString(nullnull"", chars, capacity, filePath);
 
        if (size == 0)
        {
            return null;
        }
 
        if (size < capacity - 2)
        {
            string result = new String(chars, 0, size);
            string[] sections = result.Split(new char[] { '\0' }, StringSplitOptions.RemoveEmptyEntries);
            return sections;
        }
 
        capacity = capacity * 2;
    }
}
 
public static string[] ReadKeys(string section, string filePath)
{
    // first line will not recognize if ini file is saved in UTF-8 with BOM
    while (true)
    {
        char[] chars = new char[capacity];
        int size = GetPrivateProfileString(section, null"", chars, capacity, filePath);
 
        if (size == 0)
        {
            return null;
        }
 
        if (size < capacity - 2)
        {
            string result = new String(chars, 0, size);
            string[] keys = result.Split(new char[] { '\0' }, StringSplitOptions.RemoveEmptyEntries);
            return keys;
        }
 
        capacity = capacity * 2;
    }
}
 
public static string[] ReadKeyValuePairs(string section, string filePath)
{
    while (true)
    {
        IntPtr returnedString = Marshal.AllocCoTaskMem(capacity * sizeof(char));
        int size = GetPrivateProfileSection(section, returnedString, capacity, filePath);
 
        if (size == 0)
        {
            Marshal.FreeCoTaskMem(returnedString);
            return null;
        }
 
        if (size < capacity - 2)
        {
            string result = Marshal.PtrToStringAuto(returnedString, size - 1);
            Marshal.FreeCoTaskMem(returnedString);
            string[] keyValuePairs = result.Split('\0');
            return keyValuePairs;
        }
 
        Marshal.FreeCoTaskMem(returnedString);
        capacity = capacity * 2;
    }
}

You can write values with the following code.
public static bool WriteValue(string section, string key, string value, string filePath)
{
    bool result = WritePrivateProfileString(section, key, value, filePath);
    return result;
}
Sections and keys will be created, if they not exist.

Or you can delete values with the following code.
public static bool DeleteSection(string section, string filepath)
{
    bool result = WritePrivateProfileString(section, nullnull, filepath);
    return result;
}
 
public static bool DeleteKey(string section, string key, string filepath)
{
    bool result = WritePrivateProfileString(section, key, null, filepath);
    return result;
}


Thursday, October 1, 2015

Audino - Arduino MP3-Player

There are several obstacles when creating a MP3 player. Especially if it is a special MP3 player like the Hörbert. There are some other pages that address recreating the Hörbert (here, and here), but there are still missing some information. So let's get started with the first challenge: the components.

1. Components


We need

An Arduino Uno R3 board


A MP3 shield









A rotary potentiometer







A turning knob













A toggle switch








 A battery holder









A speaker









A protective grille









Some buttons









Some caps









Some resistors









A dot matrix/strip grid board


Some wires





Some shrink tube









Some screws, nuts and spacers/standoffs

 

 2. MP3 Shield


To put the MP3 Shield on the Arduino board, soldering of headers is needed (https://learn.adafruit.com/adafruit-music-maker-shield-vs1053-mp3-wav-wave-ogg-vorbis-player/assembly).


The VS1053 library is downloaded and installed to obtain access to the "MP3 Shield" (https://learn.adafruit.com/adafruit-music-maker-shield-vs1053-mp3-wav-wave-ogg-vorbis-player/installing-software).

With the example sketch "player_simple" the MP3 Shield can be tested. Care must be taken that the "shield-example object" is created instead of the "breakout-example object".
 The output of the Serial Monitor shows the success.

3. Buttons and dot matrix/strip grid board

I want to use just one analog pin to recognize which button is pressed. Therefore several resistors are needed. At the push of a button, a parallel circuit emerges, several resistors are skipped and a voltage change occurs. The voltage change can be recognized at the analog pin, the value indicates the button.

The voltage values for the 11 buttons are shown here:


According to this table the wire layout is created. The wires are connected with the Arduino board/MP3 shield. Red with the 5V pin to supply the buttons with power, black with GND pin, and white with an analog pin to get the signal of the buttons.

Buttons and resistors will be soldered at the back.
 Some wires can be seen at the front.
The arduino board is screwed onto the dot matrix/strip grid board, also the battery holder.


Not only the buttons need a connection to the 5V pin, also the potentiometer needs to be supplied with power. Therefore another wire will be used to tap the 5V power supply.

4. Connecting the other components

The potentiometer has 3 pins. On each pin wires with male connectors are soldered. The left wire (red) is connected to the soldered 5V power supply wire. The middle wire (yellow) is connected to analog pin of the arduino board / mp3 shield. The right wire (black) is connected to a GND pin of the arduino board / mp3 shield.



The battery holder is connected to the arduino power jack. The power supply line is cut through to connect the switch button.


Two wires are soldered on the speaker. Then the wires are connected to the MP3 shield.


And so far that is the result







5. Housing

The hardware needs now some housing. Potentiometer and switch button are mounted on the top of the housing. The dot matrix/strip grid board and the speaker with the protective grille are mounted on the front. Through the back the screwed components can be accessed.



6. Sketch

The sketch let the potentiometer act as volume control, recognizes the buttons and goes to the start, next, previous, first or last mp3 track or fast-forwards the mp3 track. The current mp3 track will also be remembered, so next time the Audino is switched on the remembered track will be played again.

#include <SPI.h>
#include <Adafruit_VS1053.h>
#include <SD.h>
 
#define SHIELD_CS     7      // VS1053 chip select pin (output)
#define SHIELD_DCS    6      // VS1053 Data/command select pin (output)
#define DREQ          3      // VS1053 Data request, ideally an Interrupt pin
#define CARDCS        4      // Card chip select pin
Adafruit_VS1053_FilePlayer musicPlayer =
    Adafruit_VS1053_FilePlayer(SHIELD_CSSHIELD_DCSDREQCARDCS);
 
// VS1053 play speed parameter
#define para_playSpeed 0x1E04
 
// constants won't change
 
// the number of the pin that is used for the pushbuttons
const int buttonsPin = A0;
 
// the pin of the potentiometer that is used to control the volume
const int volumePin = A1;
 
// wait before next click is recognized
const int buttonPressedDelay = 1000;
 
 
// variables will change
 
byte currentFolder = 1;
unsigned int currentFile = 0;
unsigned int numberOfFiles[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
 
// the current volume level, set to min at start
byte volumeState = 254;
 
// last button that was pressed
byte lastPressedButton = 0;
// is the last pressed button released
boolean released = true;
// remember if the back button was pressed last time
byte lastReleasedButton = 0;
// the time at the back button was pressed last time
long lastBackButtonTime = 0;
 
char currentTrackFileName[] = "/0/current.txt";
 
// the setup routine runs once when you turn the device on or you press reset
void setup()
{
    // disable LED L
    pinMode(13, OUTPUT);
    digitalWrite(13, LOW);    
 
    // initialize serial communication at 9600 bits per second
    //Serial.begin(9600);
 
    // initialise the music player
    if (!musicPlayer.begin())
    {
        //Serial.println("VS1053 not found");
        while (1);  // don't do anything more
    }
 
    // initialise the SD card
    SD.begin(CARDCS);
 
 
    // If DREQ is on an interrupt pin (on uno, #2 or #3) we can do background
    // audio playing
    musicPlayer.useInterrupt(VS1053_FILEPLAYER_PIN_INT);  // DREQ int
 
    musicPlayer.sineTest(0x44, 100);    // Make a tone to indicate VS1053 is working 
 
    // read the number of tracks in each folder
    for (byte i = 0; i < 10; i++)
    {
        String temp = "/";
        temp.concat(i);
        char filename[3];
        temp.toCharArray(filename, sizeof(filename));
        numberOfFiles[i] = countFiles(SD.open(filename));
        //Serial.print(filename);
        //Serial.print(": ");
        //Serial.println(numberOfFiles[i]);
    }
 
    // read remembered track
    if (SD.exists(currentTrackFileName))
    {
        File file = SD.open(currentTrackFileName, FILE_READ);
        if (file)
        {
            currentFolder = file.readStringUntil('\n').toInt();
            currentFile = file.readStringUntil('\n').toInt() - 1;
        }
        file.close();
    }
 
 
    delay(100); // init delay
}
 
 
// counts the number of files in directory
unsigned int countFiles(File dir)
{
    unsigned int counter = 0;
    while (true)
    {
        File entry = dir.openNextFile();
        if (!entry)
        {
            // no more files
            break;
        }
 
        counter++;
        entry.close();
    }
    dir.close();
 
    return counter;
}
 
// the loop routine runs over and over again forever
void loop()
{
    // play next song if player stopped
    if (musicPlayer.stopped())
    {
        playNext();
    }
 
    // check the volume and set it
    checkVolume();
 
    // check if a button is pressed and perform some action
    checkButtons();
 
    delay(1); // delay in between reads for stability
}
 
 
// checks the value of the potentiometer
// if it has changed by 2 then set the new volume
void checkVolume()
{
    // read the state of the volume potentiometer
    int read = analogRead(volumePin);
 
    // set the range of the volume from max=0 to min=254
    // (limit max volume to 20 and min to 60) 
    byte state = map(read, 0, 1023, 20, 60);
 
 
    // recognize state (volume) changes in steps of two
    if (state < volumeState - 1 || state > volumeState + 1)
    {
        // remember the new volume state
        volumeState = state;
 
        // set volume max=0, min=254
        musicPlayer.setVolume(volumeState, 254);
 
        // print out the state of the volume
        //Serial.print(volumePin);
        //Serial.print(" volume ");
        //Serial.println(volumeState);
    }
}
 
// check if some button is pressed
// play first track, if button is not pressed last time
// play next track, if a button is pressed again
void checkButtons()
{
    // get the pressed button
    byte pressedButton = getPressedButton();
 
    // if a button is pressed
    if (pressedButton != 0)
    {
        //Serial.print("Taste: ");
        //Serial.println(pressedButton);
 
        // if a track/play list button is pressed
        if (pressedButton < 10 && released)
        {
            musicPlayer.stopPlaying();
            if (currentFolder == pressedButton)
            {
                playNext();
            }
            else
            {
                currentFolder = pressedButton;
                currentFile = 1;
                playCurrent();
            }
 
        }
        // if a function button is pressed
        else
        {
            if (pressedButton == 10 && released)
            {
                musicPlayer.stopPlaying();
                long time = millis();
 
                // this is the second press within 1 sec., so we 
                // got to the previous track
                if (lastReleasedButton == 10 && 
                    ((time - lastBackButtonTime) < buttonPressedDelay))
                {
                    playPrevious();
                }
                else
                {
                    playCurrent();
                }
                lastBackButtonTime = time;
            }
            else if (pressedButton == 11 && released)
            {
                // increase play speed
                musicPlayer.sciWrite(VS1053_REG_WRAMADDRpara_playSpeed);
                musicPlayer.sciWrite(VS1053_REG_WRAM, 3);
                //Serial.println("increase speed");
            }
        }
 
        released = false;
        lastReleasedButton = pressedButton;
    }
    else
    {
        released = true;
 
        // reset play speed
        if (lastPressedButton == 11)
        {
            musicPlayer.sciWrite(VS1053_REG_WRAMADDRpara_playSpeed);
            musicPlayer.sciWrite(VS1053_REG_WRAM, 1);
        }
    }
 
    // remember pressed button
    lastPressedButton = pressedButton;
}
 
 
void playPrevious()
{
    currentFile--;
    if (currentFile < 1)
    {
        currentFile = numberOfFiles[currentFolder];
    }
    playCurrent();
}
 
void playNext()
{
    currentFile++;
    if (currentFile > numberOfFiles[currentFolder])
    {
        currentFile = 1;
    }
    playCurrent();
}
 
void playCurrent()
{
    if (numberOfFiles[currentFolder] > 0)
    {
        rememberCurrentTrack();
 
        String temp = "/";
        temp.concat(currentFolder);
        temp.concat("/");
        temp.concat(currentFile);
        temp.concat(".mp3");
        char filename[temp.length() + 1];
        temp.toCharArray(filename, sizeof(filename));
        musicPlayer.startPlayingFile(filename);
 
        //Serial.print("Play ");
        //Serial.println(filename);
    }
}
 
void rememberCurrentTrack()
{
    if (SD.exists(currentTrackFileName))
    {
        SD.remove(currentTrackFileName);
    }
 
    File file = SD.open(currentTrackFileName, FILE_WRITE);
    if (file)
    {
        file.println(currentFolder);
        file.println(currentFile);
    }
    file.close();
}
 
 
// returns 0 if no button is pressed,
// else the number of the pressed button is returned (1 - 11)
byte getPressedButton()
{
    int buttonsPinValue = analogRead(buttonsPin);
    byte pressedButton = 0;
 
    if (buttonsPinValue > 823)
    {
        // button 6 has a value of about 878
        pressedButton = 6;
    }
    else if (buttonsPinValue > 725)
    {
        // button 5 has a value of about 768
        pressedButton = 5;
    }
    else if (buttonsPinValue > 649)
    {
        // button 4 has a value of about 683
        pressedButton = 4;
    }
    else if (buttonsPinValue > 586)
    {
        // button 3 has a value of about 614
        pressedButton = 3;
    }
    else if (buttonsPinValue > 535)
    {
        // button 2 has a value of about 559
        pressedButton = 2;
    }
    else if (buttonsPinValue > 492)
    {
        // button 1 has a value of about 512
        pressedButton = 1;
    }
    else if (buttonsPinValue > 450)
    {
        // if no button is pressed the value is of about 473
        pressedButton = 0;
    }
    else if (buttonsPinValue > 400)
    {
        // button 8 has a value of about 427
        pressedButton = 11;
    }
    else if (buttonsPinValue > 340)
    {
        // button 10 has a value of about 372
        pressedButton = 10;
    }
    else if (buttonsPinValue > 267)
    {
        // button 9 has a value of about 307
        pressedButton = 9;
    }
    else if (buttonsPinValue > 178)
    {
        // button 8 has a value of about 228
        pressedButton = 8;
    }
    else if (buttonsPinValue > 0)
    {
        // button 7 has a value of about 128
        pressedButton = 7;
    }
    return pressedButton;
}

7. PC-Software

The Audino is now ready for use. On the micro SD card the folders "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" must be created. Several mp3 tracks can be added to the folders 1 - 9. The mp3 tracks in each folder must be numbered consecutively (1.mp3, 2.mp3 ...). Hence just one channel is used the mp3 tracks should be in mono.

I created a PC-Software to simplify the copy process. The playlists can be planed and created.






















8. Further Readings

In german:
  1. Auswahl der Komponenten
  2. Das Entwicklungsbrett
  3. Das erste Einschalten
  4. Die Entwicklungsumgebung
  5. Knöpfe (digital)
  6. Mehrere Knöpfe (digital)
  7. Mehrere Knöpfe (analog)
  8. Potentiometer
  9. Das MP3 Shield
  10. Auswahl der Komponenten 2
  11. Auswahl der Komponenten (Zusammenfassung) 
  12. Punkt-Streifenrasterplatine und Knöpfe
  13. Punkt-Streifenrasterplatine und weitere Komponenten
  14. Das Gehäuse
  15. Sketch 1 (setup-Methode)
  16. Sketch 2 (loop-Methode)
  17. Sketch 3 (Der komplette Code)
  18. PC-Software

Wednesday, September 23, 2015

Set SVN Property Ignore with TortoiseSVN

Objects that are not checked in, were marked with questionmarks by TortoiseSVN, if icon overlay is active and functional.

If you are performing a "SVN Commit..." and "Show unversioned files" is checked, then this files will be shown.
Over time the number of files and folders can be grown and you can easily lose the overview so that needed files are not checked in. Also the risk increases that files are checked in that shouldn't (like compiled code).

For example, Visual Studio creates the folder "bin" and "obj" automatically for the compiled code. But you never want to check in the folders and their content. You can ignore the folders and their content so that they will not shown, again. To do so right click on the object to ignore and then go to "TortoiseSVN→Add to ignore list". If you choose "(recursively)" the item will be ignored in this folder and all subfolders. For files you have the choice whether all files with the same extension (*.extension) or just this particular file (file.extension) is to be ignored.

Objects that are ignored, were marked by TortoiseSVN, if icon overlay is active and functional.

Wednesday, September 2, 2015

Set SVN Property External with TortoiseSVN


An already existing project can be integrated into a new main project with the help of Subversion. This has several advantages:
  • No need to make a local copy (but be carefull with changes)
  • You can choose the version of the existing project, always the newest or fixed at some revision
  • You can use relative paths that can be predefined by the structure of the main project
If you are using a certain revision, then you prevent that changes in external project affect negatively on your project.

You can include an existing project with the property "svn:externals". This can be done at the root of the new main project.

In the context menu go to "TortoiseSVN → Properties→New...→Externals→New...". Then you have to set the "Local path" to which the external project should be checked out and the URL of the external project. If you always want to have the newest version of the external project choose "HEAD revision", otherwise choose "Revision" and set the "Peg" revision. Furthermore you can also set the "Operative" revision, if it differs.



With "SVN Update" the external projects will be checked out to the defined path.

Monday, August 31, 2015

Edit Subversion Properties with TortoiseSVN

In subversion you can set properties for versioned files and folders. There are some pre-defined properties that start e.g. with "svn:". To read and write properties in TortoiseSVN, you can use the context menu. Right click on some file or folder and then go to "TortoiseSVN → Properties".
In the upcoming diolog you can see and modify the properties that exist already. With "New..." you can set new properties. Some properties can then be selected and configured directly. To obtain all properties choose "Other".
A new dialog appears. Here you can select the "Property name". In "Property value" you can set the needed value.
You can set the properties either for a folder or for one or more files. If you set it for a folder you can set the properties optionally to all subfolders and containing files. To do that check "Apply property recursively".

Saturday, July 25, 2015

Minimum column width in WPF ListView/GridView

It seems that it is not possible to set the minimum width of a column within XAML. But it can be forced withn code-behind. Laurent Bugnion has found the solution (http://geekswithblogs.net/lbugnion/archive/2008/05/06/wpf-listviewgridview-minimum-and-maximum-width-for-a-column.aspx).

The ListView is defined in the XAML part.
 
            <ListView x:Name="MyListView">
                <!-- ... -->
            <ListView.ContextMenu>

In the constructor of the code-behind part the event handler is added.
 
            ResultDataView.AddHandler(Thumb.DragDeltaEvent,
                new DragDeltaEventHandler(Thumb_DragDelta), true);

The event handler takes care that column width is not set under 20 (in this example).
 
        private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
            Thumb senderAsThumb = e.OriginalSource as Thumb;
            GridViewColumnHeader header = senderAsThumb.TemplatedParent as
                                                            GridViewColumnHeader;
            if (header == null)
            {
                return;
            }
 
            if (header.Column.ActualWidth < 20)
            {
                header.Column.Width = 20;
            }
        }


Tuesday, June 23, 2015

Setting the Background of a WPF TextBox depending on Validation in a Template/Style using TemplateBinding

Recently I wanted to change the Background of a TextBox, if some own ValidationRule failed. But I couldn't get this to work. I tried several things and I gained more insights into styling and especially into styling wrong validated elements. And finally I get it to work.

If you are not familiar with WPF validation, here you can find excellent descriptions of how to use validation in WPF:

Now, I come to to initial situation.

Using ValidationRule in WPF

I had a ValidationRule, something like that.
namespace ErrorValidation
{
    public class RangeValidationRule : ValidationRule
    {
        public double Min { getset; }
        public double Max { getset; }
 
        public override ValidationResult Validate(object value,
            CultureInfo cultureInfo)
        {
            double enteredValue;
            if (double.TryParse(value.ToString(), out enteredValue))
            {
                if ((enteredValue < Min) || (enteredValue > Max))
                {
                    return new ValidationResult(false,
                      string.Format("Entered value must be in the range [{0};{1}].",
                      Min, Max));
                }
            }
            else
            {
                return new ValidationResult(false,
                    string.Format("Value '{0}' is not of type double.", value));
            }
 
            return new ValidationResult(truenull);
        }
    }
}
To get access to the ValidationRule the XML namespace is defined in the XAML.
xmlns:validation="clr-namespace:ErrorValidation"
I get access to the ValidationRule in the Window/UserControl Resources.
<validation:RangeValidationRule x:Key="RangeValidationRule" />
Now I can use the ValidationRule to validate user input.
        <TextBox Margin="12,0,12,12">
            <TextBox.Text>
                <Binding Path="InputField1"
                         UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <validation:RangeValidationRule Min="5"
                                                        Max="10" />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
The default style is used to mark user input that violates the ValidationRule.

Validation styles

So far, so good. But now I wanted to extend my existing TextBox style. If the validation failed, I wanted to change the Background of the TextBox. This was one of my not working tries.
        <Style x:Key="MyWrongTextBoxStyle"
               TargetType="{x:Type TextBox}">
            <Setter Property="Margin"
                    Value="4" />
            <Setter Property="Foreground"
                    Value="DarkBlue" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBoxBase}">
                        <Border Name="Border"
                                CornerRadius="2"
                                BorderThickness="1"
                                Background="White"
                                BorderBrush="LightBlue">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal" />
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <ColorAnimationUsingKeyFrames
                                                Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty=
                                                "(Panel.Background).
                                                (SolidColorBrush.Color)">
                                                <EasingColorKeyFrame
                                                    KeyTime="0"
                                                    Value="LightGray" />
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="ReadOnly">
                                        <Storyboard>
                                            <ColorAnimationUsingKeyFrames
                                                Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty=
                                                "(Panel.Background).
                                                (SolidColorBrush.Color)">
                                                <EasingColorKeyFrame
                                                    KeyTime="0"
                                                    Value="LightBlue" />
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="MouseOver" />
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <ScrollViewer Margin="0"
                                          x:Name="PART_ContentHost" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel>
                            <DockPanel>
                                <AdornedElementPlaceholder x:Name="Placeholder" />
                                <TextBlock Foreground="Red"
                                           FontSize="18"
                                           Text="!" />
                            </DockPanel>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Foreground="Red"
                                           Text="The value '" />
                                <TextBlock Foreground="Red"
                                           Text="{Binding ElementName=Placeholder,
                                                      Path=AdornedElement.Text}" />
                                <TextBlock Foreground="Red"
                                           Text="' causes an error (see tooltip)" />
                            </StackPanel>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError"
                         Value="True">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource=
                              {x:Static RelativeSource.Self},
                              Path=(Validation.Errors).CurrentItem.ErrorContent}" />
                    <Setter Property="Background"
                            Value="Orange" />
                    <Setter Property="BorderThickness"
                            Value="2" />
                    <Setter Property="BorderBrush"
                            Value="Red" />
                </Trigger>
            </Style.Triggers>
        </Style>

I tried several combinations with Validation.ErrorTemplate and Validation.HasError. But when should I use Validation.ErrorTemplate and when Validation.HasError? I have not thought much about it, until now. But the answer is not very surprising.

Validation.ErrorTemplate is used to add elements that decorate an existing element. So I cannot use it to change the Background of an existing element. But it can be used to add an image, an exclamation mark, TextBlock or something else next to the evaluated element.


Validation.HasError can be used to add a ToolTip or to change properties of an existing element.
So I should use it to change the color of the Background. But why is it not working?

The problem is how the Template style is defined. If I remove the Setter of the Template property in the Style, it is working. So I have to change the Template.

TemplateBinding

To allow changing a property of a defined Template, TemplateBinding is used. TemplateBinding is a markup extension. The properties that are set by TemplateBinding in the Template Setter can be referrenced outside the Setter of the Template. The default property values of the properties that are set by TemplateBinding can be set by other Setters. It is also possible to set the values in XAML parts that are using the Style. And now it is also possible to set the values by a Trigger, like the Background as I wanted. This is the working code that changes the Background of a TextBox in case of error.
        <Style x:Key="MyTextBoxStyle"
               TargetType="{x:Type TextBox}">
            <Setter Property="Margin"
                    Value="4" />
            <Setter Property="Foreground"
                    Value="DarkBlue" />
            <Setter Property="Background"
                    Value="White" />
            <Setter Property="BorderBrush"
                    Value="LightBlue" />
            <Setter Property="BorderThickness"
                    Value="1" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBoxBase}">
                        <Border Name="Border"
                                CornerRadius="2"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal" />
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <ColorAnimationUsingKeyFrames
                                                Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty=
                                                "(Panel.Background).
                                                (SolidColorBrush.Color)">
                                                <EasingColorKeyFrame
                                                    KeyTime="0"
                                                    Value="LightGray" />
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="ReadOnly">
                                        <Storyboard>
                                            <ColorAnimationUsingKeyFrames
                                                Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty=
                                                "(Panel.Background).
                                                (SolidColorBrush.Color)">
                                                <EasingColorKeyFrame
                                                    KeyTime="0"
                                                    Value="LightBlue" />
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="MouseOver" />
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <ScrollViewer Margin="0"
                                          x:Name="PART_ContentHost" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel>
                            <DockPanel>
                                <AdornedElementPlaceholder x:Name="Placeholder" />
                                <TextBlock Foreground="Red"
                                           FontSize="18"
                                           Text="!" />
                            </DockPanel>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Foreground="Red"
                                           Text="The value '" />
                                <TextBlock Foreground="Red"
                                           Text="{Binding ElementName=Placeholder,
                                                      Path=AdornedElement.Text}" />
                                <TextBlock Foreground="Red"
                                           Text="' causes an error (see tooltip)" />
                            </StackPanel>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError"
                         Value="True">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource=
                              {x:Static RelativeSource.Self},
                              Path=(Validation.Errors).CurrentItem.ErrorContent}" />
                    <Setter Property="Background"
                            Value="Orange" />
                    <Setter Property="BorderThickness"
                            Value="2" />
                    <Setter Property="BorderBrush"
                            Value="Red" />
                </Trigger>
            </Style.Triggers>
        </Style>
And this is the result.