I sometimes think of business objects that are intended to be referenced in XAML files through bindings as binding servers. They expose public properties and fire PropertyChanged events when these properties change.
For example, suppose you
want to display the current time in a Windows Phone 7 application, and
you want to be fairly flexible about what you display. Perhaps sometimes
you only want to display seconds, and you want to do this entirely in
XAML. For example, you might want a bit of XAML that says “The current
seconds are …” followed by a number that changes every second. The
technique I’ll show you here can be extended to many other types of
applications beyond clocks, of course.
Although you’ll want to implement the visuals entirely in XAML, you’re going to need some code—perhaps a class named simply Clock that has properties named Year, Month, Day, DayOfWeek, Hour, Minute, and Second. We’ll instantiate this Clock class in a XAML file and access the properties through data bindings.
As you know, there already is a structure in .NET that has properties with the names Year, Month, Day, and so forth. It’s called DateTime. Although DateTime is essential for writing the Clock class, it’s not quite satisfactory for our purposes because the properties in DateTime don’t dynamically change. Each DateTime object represents a particular immutable date and time. In contrast, the Clock
class I’ll show you has properties that change to reflect the current
moment, and it will notify the external world about these changes
through the PropertyChanged event
This Clock class is in the Petzold.Phone.Silverlight library. Here it is:
Example 1. Silverlight Project: Petzold.Phone.Silverlight File: Clock.cs
using System; using System.ComponentModel; using System.Windows.Threading;
namespace Petzold.Phone.Silverlight { public class Clock : INotifyPropertyChanged { int hour, min, sec; DateTime date;
public event PropertyChangedEventHandler PropertyChanged;
public Clock() { OnTimerTick(null, null);
DispatcherTimer tmr = new DispatcherTimer(); tmr.Interval = TimeSpan.FromSeconds(0.1); tmr.Tick += OnTimerTick; tmr.Start(); }
public int Hour { protected set { if (value != hour) { hour = value; OnPropertyChanged(new PropertyChangedEventArgs("Hour")); } } get { return hour; } }
public int Minute { protected set { if (value != min) { min = value; OnPropertyChanged(new PropertyChangedEventArgs("Minute")); } } get { return min; } }
public int Second { protected set { if (value != sec) { sec = value; OnPropertyChanged(new PropertyChangedEventArgs("Second")); } } get { return sec; } }
public DateTime Date { protected set { if (value != date) { date = value; OnPropertyChanged(new PropertyChangedEventArgs("Date")); } } get { return date; } }
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args) { if (PropertyChanged != null) PropertyChanged(this, args); }
void OnTimerTick(object sender, EventArgs args) { DateTime dt = DateTime.Now; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; Date = DateTime.Today; } } }
|
The Clock class implements INotifyPropertyChanged and therefore includes a public event named PropertyChanged. Near the bottom, a protected OnPropertyChanged
method is also included and is responsible for firing the actual event.
The constructor of the class installs a handler for the Tick event of the DispatcherTimer initialized to an interval of 1/10th second. The OnTimerTick handler (at the very bottom of the class) sets new values of the class’s Hour, Minute, Second, and Date properties, all of which are structured very similarly.
For example, look at the Hour property:
public int Hour
{
protected set
{
if (value != hour)
{
hour = value;
OnPropertyChanged(new PropertyChangedEventArgs("Hour"));
}
}
get
{
return hour;
}
}
The set accessor is protected. The value is only set internally and we don’t want external classes messing with it. The set accessor checks if the value being set to the property equals the value stored as a field; if not, it sets the hour field to the new value and calls OnPropertyChanged to fire the event.
Some programmers don’t include the if statement to check that the property is actually changing, with the result that the PropertyChanged
event is fired whenever the property is set, even if it’s not changing.
That’s not a good idea—particularly for a class like this. We really
don’t want a PropertyChanged event reporting that the Hour property is changing every 1/10th second if it’s really changing only every hour.
To use the Clock class in a XAML file, you’ll need a reference to the Petzold.Phone.Silverlight library and an XML namespace declaration:
xmlns:petzold="clr-namespace:Petzold.Phone.Silverlight;assembly=Petzold.Phone.Silverlight"
When a binding source is not derived from DependencyObject, you don’t use ElementName In the Binding. Instead, you use Source. The bindings we want to create set Source to the Clock object in the Petzold.Phone.Silverlight library.
You can insert a reference to the Clock class directly in the element form of Binding:
<TextBlock>
<TextBlock.Text>
<Binding Path="Second">
<Binding.Source>
<petzold:Clock />
</Binding.Source>
</Binding>
</TextBlock.Text>
</TextBlock>
The Source property of Binding is broken out as a property element and set to an instance of the Clock class. The Path property indicates the Second property of Clock.
Or, more conventionally, you define the Clock as a XAML resource:
<phone:PhoneApplicationPage.Resources>
<petzold:Clock x:Key="clock" />
. . .
</phone:PhoneApplicationPage.Resources>
Then the Binding markup extension can reference that resource:
TextBlock Text="{Binding Source={StaticResource clock}, Path=Second}" />
Notice the embedded markup expression for StaticResource.
This approach is demonstrated in the TimeDisplay project, which uses a horizontal StackPanel to concatenate text:
Example 2. Silverlight Project: TimeDisplay File: MainPage.xaml
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="The current seconds are " /> <TextBlock Text="{Binding Source={StaticResource clock}, Path=Second}" /> </StackPanel> </Grid>
|
And here it is:
To re-emphasize: The binding target (the Text property of the TextBlock) must be a dependency property. That is required. To keep the target updated with changing values from the binding source (the Second property of Clock), the source should implement some kind of notification mechanism, which it does.
Of course, I don’t need the StackPanel with the multiple TextBlock elements. Using the StringFormatConverter (which I’ve included as a resource in TimeDisplay with a key of “stringFormat” so you can experiment with it) I can simply include the whole text like so:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Source={StaticResource clock},
Path=Second,
Converter={StaticResource stringFormat},
ConverterParameter='The current seconds are {0}'}" />
</Grid>
Now the Binding markup expression has two embedded markup expressions.
If you want to display several properties of the Clock class, you’ll need to go back to using multiple TextBlock
elements. For example, this will format the time with colons between
the hours, minutes, and seconds and leading zeros for the minutes and
seconds:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="{Binding Source={StaticResource clock},
Path=Hour}" />
<TextBlock Text="{Binding Source={StaticResource clock},
Path=Minute,
Converter={StaticResource stringFormat},
ConverterParameter=':{0:D2}'}" />
<TextBlock Text="{Binding Source={StaticResource clock},
Path=Second,
Converter={StaticResource stringFormat},
ConverterParameter=':{0:D2}'}" />
</StackPanel>
</Grid>
As you can see, the three bindings all include the same Source setting. Is there some way that allows us to avoid the repetition? Yes there is, and the technique also illustrates an extremely important concept.