text:
Topic List:
- Binding to an Object and Properties
- Control Templates
- Data Validation
- Change Notification - INotifyPropertyChanged
I have made the following changes to the application from Part 3. None of these are essential so you could skip them if you prefer. It is also possible to complete this part of the tutorial with a new Window, two TextBoxes and a DatePicker; it just requires the addition of the reference local:
xmlns:local="clr-namespace:ProgrammersWPF2", and a few other minor changes.
Changes to the Application:
- Moved Menu and MenuItem Styles into App.xaml
- Removed Canvas, and Expander & Canvas
- Removed a ColumnDefinition
- Removed btnClose_Click handler from code-behind (using Command instead)
- Renamed the DatePicker from 'dtpStartDate' to 'dpStartDate' (because it bugged me)
What will be added?
- We will bind the Window to a Programmer object and its properties
- Validation so that the StartDate must occur in the future
- Visual feedback if the StartDate doesn't validate
- Changes to a Programmer's properties will be notified to the GUI, causing it to update.
Binding to an Object and Properties
The following is a very useful binding overview (I have borrowed some validation code from it):
Data Binding Overview :MSDN
I recommend studying it, perhaps after completing this part of my tutorial. It is, as I say, very useful, but I think you need to download the sample application as well, to get a better picture of how the various parts fit together.
We will bind our Window to a Programmer instance. The TextBoxes will be bound to FirstName and LastName properties of this object, and the DatePicker to its StartDate property. In a later section we will add change notification so that the GUI is notified of changes to the object's state, so that it can update automatically.
Create a new Class file Programmer.cs in our Project:
using System; namespace ProgrammersWPF2 { class Programmer { public string FirstName { get; set; } public string LastName { get; set; } public DateTime? StartDate { get; set; } } }
Note that this is still in the same namespace as our Project so we can still reference it in the XAML using local:. (Recall that we added xmlns:local="clr-namespace:ProgrammersWPF2" to the Window element.)
Notice also that the StartDate is nullable. Accounting for null was very tricky with a Winform-DateTimePicker. It is much less of an issue with a WPF-DatePicker; however, we still need to be aware of a possible null-value when performing validation against the date.
Add the following Resource to Mainwindow.xaml:
<window.Resources> <local:Programmer x:Key="programmer" /> </window.Resources>
This will create a new Programmer-instance when we run the application, which we can reference in the XAML as programmer.
One, of several, alternatives is to provide a DataContext on the Grid:
<Grid DockPanel.Dock="Top" DataContext="local:Programmer x:Key=programmer"> <!-- then we can just use Path --> <TextBox.Text> <Binding Path="FirstName" /> </TextBox.Text>
Modify the FirstName-TextBox as follows:
<TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" x:Name="txtFirstName"> <TextBox.Text> <Binding Source="{StaticResource programmer}" Path="FirstName" /> </TextBox.Text> </TextBox>
This binds the Text of the TextBox to the FirstName property of the Programmer instance. Do the same for LastName:
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" x:Name="txtLastName"> <TextBox.Text> <Binding Source="{StaticResource programmer}" Path="LastName" /> </TextBox.Text> </TextBox>
While we're at it we can also modify the StatusBar to bind to these properties:
<StatusBar DockPanel.Dock="Bottom"> <TextBlock>Full Name: <TextBlock Text="{Binding Source={StaticResource programmer}, Path=FirstName}" /> <TextBlock Text="{Binding Source={StaticResource programmer}, Path=LastName}" /> </TextBlock> </StatusBar>
If you now run the application you probably won't notice any difference. However, when you type a FirstName or LastName the StatusBar doesn't update until you move away from the TextBox. By default, TextBoxes do not update their bound-property until they lose the Focus.
Most controls, such as CheckBoxes and ListBoxes, will update their bound-property immediately by default, on checking or selecting an item. For TextBoxes it is sensible for the updating to be delayed until the box loses focus, otherwise it could be a significant drain to update with every key-press. We can change this behaviour using the Binding.UpdateSourceTrigger Property.
Modify the StartDate-DatePicker:
<DatePicker Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" x:Name="dpStartDate"> <DatePicker.SelectedDate> <Binding Source="{StaticResource programmer}" Path="StartDate" /> </DatePicker.SelectedDate> </DatePicker>
Notice that it is the SelectedDate that is bound to the StartDate property, not the DatePicker itself.
Run the application again to test this.
Side Note: We still have the following binding as an attribute of the Window:
FocusManager.FocusedElement="{Binding ElementName=txtFirstName}"
Recall that this moves the focus (the cursor) into the FirstName-TextBox when the application is run. This is a pure-UI binding and has nothing to do with our data (our model). The same applies to the CheckBox in the ToolBar (Toggled?), mainly because it doesn't do anything currently.
Control Templates
ControlTemplate Class :MSDN
MSDN said:
Specifies the visual structure and behavioral aspects of a Control that can be shared across multiple instances of the control.
Styling and Templating :MSDN
Customizing the Appearance of an Existing Control by Creating a ControlTemplate :MSDN
You should study these links, and their examples, as I won't be covering this topic in great detail. My aim is to use a ControlTemplate to provide visual-feedback if validation of the StartDate fails.
However, here is an example I found that applies to a ListBox Control:
A ControlTemplate is typically part of a Style. It defines the visual structure of a Control. This doesn't just mean formatting, but the actual structure of the Control - how it is constituted.
(I haven't investigated how accurate this image is, but it does convey a sense of how detailed ControlTemplates can be.)
In the example (in the spoiler) ItemsPresenter specifies where the list-items sit within the ListBox Control. TemplateBinding is used to link properties in the template to properties of the control itself. In this instance it links, for example, the Background property to the Background property defined by the Style (LightGray).
Add the following ControlTemplate to our App.xaml file:
<ControlTemplate x:Key="validationTemplate"> <DockPanel> <TextBlock Foreground="Red" FontSize="20">!</TextBlock> <AdornedElementPlaceholder/> </DockPanel> </ControlTemplate>
We will use this template to place a red exclamation-mark to the side of a Control that doesn't validate. AdornedElementPlaceholder specifies where the Control itself (that we have applied this template to) will appear.
(This template is not essential because, by default, a red border will automatically appear around the element.)
Data Validation
Add the following new DatePicker-Style to App.xaml:
<Style x:Key="styDPFuture" TargetType="DatePicker" BasedOn="{StaticResource controlStyle}"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" /> </Trigger> </Style.Triggers> </Style>
This will display the [first] validation-error message as a ToolTip. (I've created this as a new Style as perhaps not every DatePicker will require validation.)
We will now sub-class ValidationRule in a new class named FutureDateRule.cs. This provides our validation-logic.
using System; using System.Windows.Controls; using System.Globalization; namespace ProgrammersWPF2 { class FutureDateRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { DateTime date; try { if (value == null) { return ValidationResult.ValidResult; // StartDate is nullable } date = DateTime.Parse(value.ToString()); } catch (FormatException) { return new ValidationResult(false, "Value is not a valid date."); } if (DateTime.Now.Date > date) { return new ValidationResult(false, "Please enter a date in the future."); } else { return ValidationResult.ValidResult; } } } }
Note that this is not the only way to validate data. Here is an example using IDataInfoError. It is also possible to throw Exceptions.
This code is taken from the Binding Overview I referred to earlier, but I've added an additional check for a null-value. This is necessary because value.ToString() raises a NullReferenceException if value is null.
The first argument of ValidationResult() is 'isValid' which confirms if the validation failed. ValidResult returns a valid instance of ValidationResult: in English, it confirms that the validation passed, so no error needs to be generated (and an existing error can be removed).
Finally, we hook-up the ControlTemplate, Style and ValidationRule in the XAML for the DatePicker:
<DatePicker Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" x:Name="dpStartDate" Validation.ErrorTemplate="{StaticResource validationTemplate}" Style="{StaticResource styDPFuture}"> <DatePicker.SelectedDate> <Binding Source="{StaticResource programmer}" Path="StartDate"> <Binding.ValidationRules> <local:FutureDateRule /> </Binding.ValidationRules> </Binding> </DatePicker.SelectedDate> </DatePicker>
The ErrorTemplate is applied to the DatePicker itself, but the Binding, and Binding.Validation, apply to its SelectedDate.
Run and test the application. Enter a StartDate before today's date to see this in action, and point at the DatePicker to see the ToolTip.
You probably won't see the ToolTip "Value is not a valid date." because the DatePicker just reverts to the last valid date (or a placeholder) if an invalid date is entered. If we were using a TextBox you would be able to produce this message. In fact, we can apply our validation to a TextBox (or any other Control) without changing any of the code, just some XAML.
Change Notification - INotifyPropertyChanged
How to: Implement Property Change Notification :MSDN
We will modify our Programmer Class so that property changes (changes of state) are automatically notified to the Window, causing it to update. (More accurately, the property raises an event, notifying any subscribers (to this event) that its property-value has changed.)
INotifyPropertyChanged is covered well in this tutorial (Properties).
Modify Programmer.cs to this:
using System; using System.ComponentModel; namespace ProgrammersWPF2 { class Programmer : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _firstName; private string _lastName; private DateTime? _startDate; public string FirstName { get { return _firstName; } set { if (_firstName == value) return; _firstName = value; RaisePropertyChanged("FirstName"); } } public string LastName { get { return _lastName; } set { if (_lastName == value) return; _lastName = value; RaisePropertyChanged("LastName"); } } public DateTime? StartDate { get { return _startDate; } set { if (_startDate == value) return; _startDate = value; RaisePropertyChanged("StartDate"); } } private void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } }
(Note that the StatusBar still won't update immediately because the First and LastName properties are not changed until their TextBoxes lose focus.)
With our little application it is not obvious that this is working. If you wish to show that it is then add a Button:
<Button Grid.Row="3" Grid.Column="6" Content="Temp" Click="Button_Click" />
and in the code-behind add this:
private void Button_Click(object sender, RoutedEventArgs e) { Programmer pro = (Programmer)this.FindResource("programmer"); pro.FirstName = "Dave"; }
Clicking the Button changes the FirstName property of the Programmer instance, which is immediately reflected in the FirstName-TextBox. This code also demonstrates how to refer to a Resource in the code-behind. FindResource
Data Binding Overview :MSDN
Your next step could be to investigate Binding to Collections. Then either attempt a small application, or study the MVVM pattern first.
WPF Apps With The Model-View-ViewModel Design Pattern :MSDN
You might now continue with this tutorial:
WPF Build An Application
where I begin the process of building a complete Application.
This post has been edited by andrewsw: 12 May 2014 - 04:46 PM