Displaying Data Validation Messages in WPF
As you can probably tell from my last couple posts I've been working with WPF in different data scenarios. Yesterday I was playing with data validation in WPF and .NET 3.5 which is pretty slick. In this article I'll walk through how to hook up validation in your data objects using the IDataErrorInfo interface and then I'll go through a couple Validation ErrorTemplates you can use to display the validation error messages and cues to the user.
Performing Validation on Data Objects
If you're using custom business objects or LINQ to SQL classes you first need to implement the IDataErrorInfo interface in order to collect validation messages on your objects. If you are using DataSets on your WPF or Windows Forms, the DataRowView already implements this interface so you can just add validation to your DataTable partial classes and you're good to go. Just open the DataSet designer, right-click on the DataTable and select "View Code" and party on. For instance, if we have a customer DataTable we can write validation for the LastName field like so:
Partial Class CustomerDataSet Partial Class CustomerDataTable Private Sub CheckLastName(ByVal row As CustomerRow) If row.IsNull("LastName") OrElse row.LastName = "" Then row.SetColumnError(Me.LastNameColumn, "Please enter a last name") Else row.SetColumnError(Me.LastNameColumn, "") End If End Sub Private Sub CustomerDataTable_ColumnChanged(ByVal sender As Object, _ ByVal e As System.Data.DataColumnChangeEventArgs) _ Handles Me.ColumnChanged If e.Column Is Me.LastNameColumn Then Me.CheckLastName(CType(e.Row, CustomerRow)) End If End Sub Private Sub CustomerDataTable_TableNewRow(ByVal sender As Object, _ ByVal e As System.Data.DataTableNewRowEventArgs) _ Handles Me.TableNewRow Dim row As CustomerRow = CType(e.Row, CustomerRow) 'This will fire the ColumnChanged event which will give 'immediate feedback to user when a row is added. '(Stick other default values here too.) row.LastName = "" End Sub End Class End Class
If you're building our own custom business objects or are using LINQ to SQL classes then it's up to you to implement the IDataErrorInfo interface yourself. I showed how to do this with LINQ to SQL classes in this post where I set it up in a base business class. Here's a "short version" example implementation for a customer LINQ to SQL class that performs the same validation on the LastName field:
Partial Class Customer Implements System.ComponentModel.IDataErrorInfo 'This dictionary contains a list of our validation errors for each field Private validationErrors As New Dictionary(Of String, String) Protected Sub AddError(ByVal columnName As String, ByVal msg As String) If Not validationErrors.ContainsKey(columnName) Then validationErrors.Add(columnName, msg) End If End Sub Protected Sub RemoveError(ByVal columnName As String) If validationErrors.ContainsKey(columnName) Then validationErrors.Remove(columnName) End If End Sub Public Overridable ReadOnly Property HasErrors() As Boolean Get Return (validationErrors.Count > 0) End Get End Property Public ReadOnly Property [Error]() As String _ Implements System.ComponentModel.IDataErrorInfo.Error Get If validationErrors.Count > 0 Then Return String.Format("{0} data is invalid.", TypeName(Me)) Else Return Nothing End If End Get End Property Default Public ReadOnly Property Item(ByVal columnName As String) As String _ Implements System.ComponentModel.IDataErrorInfo.Item Get If validationErrors.ContainsKey(columnName) Then Return validationErrors(columnName).ToString Else Return Nothing End If End Get End Property Private Sub OnValidate(ByVal action As System.Data.Linq.ChangeAction) Me.CheckLastName(Me.LastName) If Me.HasErrors Then Throw New Exception(Me.Error) End If End Sub Private Sub OnLastNameChanging(ByVal value As String) Me.CheckLastName(value) End Sub Private Sub CheckLastName(ByVal value As String) If value = "" Then Me.AddError("LastName", "Please enter a last name") Else Me.RemoveError("LastName") End If End Sub End Class
Data Binding in WPF
Now that our data objects are validating themselves we can data bind them to a form. Setting up a simple WPF Window with some TextBoxes and binding them is easy in XAML once you get the knack for remembering the syntax ;-). The key is to make sure you specify the ValidatesOnDataErrors attribute on the Binding and set it to True. Take a look at the TextBoxes in the XAML below:
<Window x:Class="Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Customers" Height="253" Width="300" Name="Window1"> <Grid Margin="6"> <Grid.RowDefinitions> <RowDefinition Height="222*" /> <RowDefinition Height="40*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="112*" /> <ColumnDefinition Width="166*" /> </Grid.ColumnDefinitions> <StackPanel Name="StackPanel1"> <Label Name="Label1" Width="Auto" HorizontalContentAlignment="Right" Margin="3"> Last Name:</Label> <Label Name="Label2" Width="Auto" HorizontalContentAlignment="Right" Margin="3"> First Name:</Label> <Label Name="Label3" Width="Auto" HorizontalContentAlignment="Right" Margin="3"> City:</Label> <Label Name="Label4" Width="Auto" HorizontalContentAlignment="Right" Margin="3"> State:</Label> <Label Name="Label5" Width="Auto" HorizontalContentAlignment="Right" Margin="3"> ZIP:</Label> </StackPanel> <StackPanel Grid.Column="1" Name="StackPanel2"> <TextBox Text="{Binding Path=LastName, ValidatesOnDataErrors=True}" Name="TextBox1" Height="28" Width="Auto" HorizontalContentAlignment="Left" Margin="3" /> <TextBox Text="{Binding Path=FirstName, ValidatesOnDataErrors=True}" Name="TextBox2" Height="28" Width="Auto" HorizontalContentAlignment="Left" Margin="3" /> <TextBox Text="{Binding Path=City, ValidatesOnDataErrors=True}" Name="TextBox3" Height="28" Width="Auto" HorizontalContentAlignment="Left" Margin="3"/> <TextBox Text="{Binding Path=State, ValidatesOnDataErrors=True}" Name="TextBox4" Height="28" Width="Auto" HorizontalContentAlignment="Left" Margin="3"/> <TextBox Text="{Binding Path=ZIP, ValidatesOnDataErrors=True}" Name="TextBox5" Height="28" Width="Auto" HorizontalContentAlignment="Left" Margin="3" /> </StackPanel> <Button Name="btnAdd" Grid.Column="1" Grid.Row="1" Margin="0,0,79,6" Height="24" Width="75" VerticalAlignment="Bottom" HorizontalAlignment="Right" >Add</Button> <Button Name="btnSave" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Right" Margin="0,0,0,6" Width="75" Height="24" VerticalAlignment="Bottom">Save</Button> </Grid> </Window>
Now we can load our data and set it to the Window.DataContext in the Window_Loaded event handler. If you're using DataSets, then set up your TableAdapter query like normal and Fill the DataSet. Then set the Window's DataContext to the customer DataTable:
Me.CustomerTableAdapter.Fill(Me.MyCustomerData.Customer) Me.DataContext = Me.MyCustomerData.Customer
If you're using LINQ to SQL classes then just call upon the SQLDataContext to load your list of customers:
Dim db As New MyDatabaseDataContext Me.DataContext = From Customer In db.Customers _ Where Customer.LastName = "Massi"
WPF's Default Validation ErrorTemplate
So if we were to run this as-is WPF would give us a default visual cue when our validation fails. The control is drawn with a red border indicating there is a problem, however no message is displayed. Oh yea, that's helpful! Prepare for your tech support phone lines to light up if you release this baby.
Specifying a Custom Validation Style
We obviously want to let the user know what needs fixing here. Let's just do something simple and display the message in a ToolTip. For now, we can just create a Style in our Window.Resources section that applies to the Textboxes on this form. The Style sets up a Trigger that sets the ToolTip property to the validation message when the Validation.HasError changes to True:
<Window.Resources> <Style TargetType="TextBox"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> </Window.Resources>
Now when we run this again, you'll see that when you hover over the TextBox the validation message is displayed in a ToolTip. Better! But this solution only displays for TextBoxes. What about the rest of our controls like CheckBoxes, ComboBoxes, etc.? And we really want to declare all this in one place for our entire application. No problem, we can stick this Style into the Application.Resources instead. We can also specify that the TargetType="Control" and then we can declare additional styles for the rest of our controls and base them on this one. Open up your Application.xaml and add this XAML to your Resources section:
<Application.Resources> <Style TargetType="Control" x:Key="myErrorTemplate"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> <Style TargetType="TextBox" BasedOn="{StaticResource myErrorTemplate}" /> <Style TargetType="CheckBox" BasedOn="{StaticResource myErrorTemplate}" /> <Style TargetType="ComboBox" BasedOn="{StaticResource myErrorTemplate}" /> </Application.Resources>
We just need to specify a x:Key for the Template and then we can set the BasedOn attribute on the inherited Styles. Now all the controls in the entire application can pick up this Style.
Replacing the Entire ErrorTemplate
So far all we've done is specify a Style Trigger. The default WPF ErrorTemplate is still being utilized as we're still seeing the red border around the control. We can completely change the ErrorTemplate that is used by defining a new one here in the Application.Resources. Let's take a simple example by setting up our ErrorTemplate to display a generic message over the control. In the Style above the Trigger section (we'll leave the ToolTip mesage there) we set the Validation.ErrorTemplate property and its Value to our very own ControlTemplate.
<Style TargetType="Control" x:Key="myErrorTemplate"> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <TextBlock Foreground="Red" Text="DOH! Thank you for trying."/> </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)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style>
Now when we run this again, we still get our ToolTip when we hover over the control, but now we're also overlaying the control with TextBlock we defined in our ControlTemplate. Notice that there's no more red border:
Okay a pretty lame example, I admit. The problem (besides being a sarcastic message) is that the TextBlock is really covering the control and you have to hover over the edge to get the ToolTip to display. The other problem of course is that if we start typing into the field again the message won't disappear until we tab off if it so that's pretty annoying.
Instead you can stick a DockPanel into the ControlTemplate and Dock the TextBlock to the right in order to display the text after the control (and this time let's just display an asterisk). Say you want to still have that red border around the control. We can do this by specifying a special element called AdornedElementPlaceholder in our XAML for the ErrorTemplate Setter.Value:
<Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel LastChildFill="True"> <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="11pt" FontWeight="Bold">* </TextBlock> <Border BorderBrush="Red" BorderThickness="1"> <AdornedElementPlaceholder Name="myControl"/> </Border> </DockPanel> </ControlTemplate> </Setter.Value> </Setter>
Much better! Of course you can use your imagination to create any kind of visual cue appropriate for your application. That's the cool thing about WPF.
Duplicating the Winforms ErrorProvider Look and Feel
For those Winforms developers out there, what if you want to duplicate the look and feel of the ErrorProvider which displays a blinking error icon? I always liked to place the red error icon inside the right-hand side of the control so I didn't have to worry about spacing issues between the controls when I was designing forms. And I actually liked how the icon would flash a few times and then stop. It's relatively easy to do this type of animation in WPF using Storyboards (and it's REALLY easy to create animations in Expression Blend so I highly recommend you have a look at that product if you're making the transition to WPF).
This time we'll create an Ellipse and set up an EventTrigger for the Loaded event to begin our animation which will simply toggle the Visibility property of the Ellipse a few times. We also want to place a TextBlock over the Ellipse whose Text is an exclamation point (the animation will run on this as well). And since I want to place these inside the right-hand side of the control by setting a negative left margin, I'm going to want to also set the ToolTips of the Ellipse and the TextBlock so that if the user hovers over the error glyph it will display the ToolTip as well.
Here's the complete XAML to enable this look and feel contained in the Application.Resources:
<Application.Resources> <Storyboard x:Key="FlashErrorIcon"> <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(UIElement.Visibility)"> <DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Hidden}"/> <DiscreteObjectKeyFrame KeyTime="00:00:00.2000000" Value="{x:Static Visibility.Visible}"/> <DiscreteObjectKeyFrame KeyTime="00:00:00.4000000" Value="{x:Static Visibility.Hidden}"/> <DiscreteObjectKeyFrame KeyTime="00:00:00.6000000" Value="{x:Static Visibility.Visible}"/> <DiscreteObjectKeyFrame KeyTime="00:00:00.8000000" Value="{x:Static Visibility.Hidden}"/> <DiscreteObjectKeyFrame KeyTime="00:00:01" Value="{x:Static Visibility.Visible}"/> </ObjectAnimationUsingKeyFrames> </Storyboard> <Style x:Key="myErrorTemplate" TargetType="Control"> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel LastChildFill="True"> <Ellipse DockPanel.Dock="Right" ToolTip="{Binding ElementName=myTextbox, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" Width="15" Height="15" Margin="-25,0,0,0" StrokeThickness="1" Fill="Red" > <Ellipse.Stroke> <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5"> <GradientStop Color="#FFFA0404" Offset="0"/> <GradientStop Color="#FFC9C7C7" Offset="1"/> </LinearGradientBrush> </Ellipse.Stroke> <Ellipse.Triggers> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard Storyboard="{StaticResource FlashErrorIcon}"/> </EventTrigger> </Ellipse.Triggers> </Ellipse> <TextBlock DockPanel.Dock="Right" ToolTip="{Binding ElementName=myControl, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" Foreground="White" FontSize="11pt" Margin="-15,5,0,0" FontWeight="Bold">! <TextBlock.Triggers> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard Storyboard="{StaticResource FlashErrorIcon}"/> </EventTrigger> </TextBlock.Triggers> </TextBlock> <Border BorderBrush="Red" BorderThickness="1"> <AdornedElementPlaceholder Name="myControl"/> </Border> </DockPanel> </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)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> <Style TargetType="TextBox" BasedOn="{StaticResource myErrorTemplate}" /> <Style TargetType="CheckBox" BasedOn="{StaticResource myErrorTemplate}" /> <Style TargetType="ComboBox" BasedOn="{StaticResource myErrorTemplate}" /> </Application.Resources>
Now when we run our application and trigger the validation error we see an error icon that flashes 3 times (it looks a lot smoother than this image ;-))
Validating your data objects in WPF with the .NET Framework 3.5 is the same as before with WinForms using the IDataErrorInfo interface. However, WPF styles and control templates make displaying visual cues to the user extremely flexible. If you can imagine it, you can probably do it with WPF.
Enjoy!
原文地址:http://blogs.msdn.com/bethmassi/archive/2008/06/27/displaying-data-validation-messages-in-wpf.aspx