He,YuanHui —— 业精于勤荒于嬉,行成于思毁于随

如果你喜欢一个事,又有这样的才干,那就把整个人都投入进去,就要象一把刀直扎下去直到刀柄一样,不要问为什么,也不要管会碰到什么。

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Displaying Data Validation Messages in WPF

27 June 08 03:57 PM

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

posted on 2008-09-03 14:58  He,YuanHui  阅读(1572)  评论(0编辑  收藏  举报

Add to Google