6 Things I Bet You Didn't Know About Data Binding in WPF
Posted
Wednesday, January 21, 2009 3:26 PM
by
Nathan Zaugg
I fell in love with WPF the first time I saw it in action! WPF is a thing of pure beauty! Anyway, from the second I saw how the data binding worked I knew that there was going to be a lot to figure out there! It also seems as though there is no real data binding expert -- or the experts don't blog too much about it. I created my XAML Cheat Sheet shortly after feeling like I had some kind of grasp on the concept. It is still a great reference for basic bindings, but when you are real serious about binding you'll have to do better than that.
So I have been working on a project in my spare time called SmartForms 2.0 (more on that later) and I have had to do some very difficult things using bindings. In my previous experience with WPF I would either create a cleaver type converter and/or just give up and set the value/state of the controls in code (eww!). This time is different and I have to make data binding work.
Example #1
Lets say that you need to bind a bool? (nullable boolean) type to a checkbox.
<CheckBox IsChecked="{Binding SomeBooleanField}">Checkbox</CheckBox>
Produces:
You will notice that the checkbox is in a neither checked nor unchecked state. Setting IsThreeState="False" doesn't help you as it is false by default. I want the checkebox to be unchecked by default but have no control over the type or the initialization of the type. Check this out:
<CheckBox IsChecked="{Binding SomeBooleanField,
TargetNullValue=false}">
Checkbox
</CheckBox>
And now we have normal looking checkboxes! The best part is that this method doesn't mutate our property. This was exactly what I was looking for!
Example #2
The following XAML is pretty straight forward. It is simply a text box that we expect to put a city into.
<StackPanel Width="130" Height="40" HorizontalAlignment="Left"
VerticalAlignment="Top">
<TextBlock>City</TextBlock>
<TextBox Text="{Binding SomeTextField}" />
</StackPanel>
Produces:
What if the item we are attempting to data bind to is not available. It's not going to be null, the binding will just fail. This should work well:
<StackPanel Width="130" Height="40" HorizontalAlignment="Left"
VerticalAlignment="Top">
<TextBlock>City</TextBlock>
<TextBox Text="{Binding SomeTextField,
FallbackValue=UNBOUND}" />
</StackPanel>
Produces:
Example #3
This is a much more common occurrence. You have a number that you need to format in a textbox. This example shows the decimal value with no real format at all.
<StackPanel Width="130" Height="40" HorizontalAlignment="Left"
VerticalAlignment="Top">
<TextBlock>Cost</TextBlock>
<TextBox Text="{Binding SomeDecimalField}" />
</StackPanel>
Produces:
There are a few ways to get the format we desire. The first example uses a static resource:
xmlns:clr="clr-namespace:System;assembly=mscorlib"
<StackPanel Width="130" Height="40" HorizontalAlignment="Left"
VerticalAlignment="Top">
<StackPanel.Resources>
<clr:String x:Key="formatStr">{0:C}</clr:String>
</StackPanel.Resources>
<TextBlock>Cost</TextBlock>
<TextBox Text="{Binding SomeDecimalField,
StringFormat={StaticResource formatStr}}" />
</StackPanel>
You can put the string inline but it looks really funny and for some reason the there has to be a non-whitespace character before the formatting expression which you may not want:
<TextBox Text="{Binding SomeDecimalField,
StringFormat=Cost: {0:C}}" />
This formats the string to look like "Cost: $1,2398.00". You can see how that could cause lots of confusion and would be undesirable. There is a way we can get the binding to work a little better:
<TextBox Text="{Binding SomeDecimalField,
StringFormat={}{0:C}}" />
OR
<TextBox>
<TextBox.Text>
<Binding Path="SomeDecimalField"
StringFormat="{}{0:C}" />
</TextBox.Text>
</TextBox>
The result is a nicely formatted output:
Notice how the binding was applied? This is how you apply MultiBindings which is the next topic.
Example #4
What if you want the value of one or more fields to exist in a single binding? That is where MultiBindings shine! Check out this very easy example:
<StackPanel Width="130" Height="40" HorizontalAlignment="Left"
VerticalAlignment="Top">
<TextBlock>Name</TextBlock>
<TextBox>
<TextBox.Text>
<MultiBinding StringFormat="{}{0}, {1}">
<Binding Path="LastName" />
<Binding Path="FirstName" />
</MultiBinding>
</TextBox.Text>
</TextBox>
</StackPanel>
Results:
Example #5
Validation can happen at the point of binding. This can be handy if the objects you are binding to have logic built in. For example, lets say you have a property for birth date. Obviously the person can not be born in the future! Here is how we avoid such rotten input!
public DateTime birthdate;
public DateTime BrithDate {
get { return birthdate; }
set {
if (value > DateTime.Today)
throw new ArgumentException(
"Person can not be born in the future!");
birthdate = value;
}
}
And the XAML:
<StackPanel Margin="5" Width="130" Height="40"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<TextBlock>Birthdate</TextBlock>
<TextBox Text="{Binding BrithDate,
Mode=TwoWay,
StringFormat=d,
NotifyOnValidationError=true,
ValidatesOnExceptions=true}" />
</StackPanel>
Result:
The error can also be styled if required. Shown is the default style (nothing custom).
Example #6
The last example is for something called PriorityBinding. This is poorly named in my opinion as it would be better called CascadingBinding. The point to PriorityBinding is to name multiple data bindings in order of most desirable to least desirable. This way if the first binding fails, is empty and/or default, another binding can take it's place. You may have to be a little imaginative to think of such a scenario, but this kind of thing could be useful so I'm glad it's there. Just for fun, I am using the IsAsync field as well. This tells WPF to use a thread other than the GUI thread to retrieve this property. This is important for properties that take a long time to return as it will leave your app unresponsive until the value is bound. In the case of this example without that attribute the window will not show until the 5 seconds had elapsed.
private string fname;
public string FirstName {
get {
Thread.Sleep(5000);
return fname;
}
set { fname = value; }
}
Xaml:
<StackPanel Margin="5" Width="130" Height="40"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<TextBlock>Name</TextBlock>
<TextBox>
<TextBox.Text>
<PriorityBinding>
<Binding Path="LastNameNonExistant"
IsAsync="True" />
<Binding Path="FirstName"
IsAsync="True" />
</PriorityBinding>
</TextBox.Text>
</TextBox>
</StackPanel>
Result:
Another neat little thing to know is that the Hierarchy for Binding is Binding -> BindingBase -> MarkupExtension -> Object. Unlike much of the rest of the .NET Framework you can actually inherit from any of these classes and provide support for your customized binding needs. That is exactly what I am doing for SmartForms 2.0! I created a binding class called DataBinding and it looks a little like this:
xmlns:db="clr-namespace:SmartForms2.Binding;assembly=SmartForms2"
<TextBox Name="firstName" Width="175"
Text="{db:DataBinding Data, DataSource=DB1}" />
My DataBinding class inherits directly from MarkupExtension so all I had to do is implement the abstract method ProvideValue.
Links: