Validate User Input

来源:互联网 发布:雅马哈 电钢琴 知乎 编辑:程序博客网 时间:2024/05/17 00:19

For most programs, displaying data is only half of the battle. The other big challenge is analyzing, accepting, and rejecting data entered by the user. In an ideal world, where all users always enter logical and accurate data, this would be a simple task. In the real world, however, this is not at all the case.

It is our job, as developers and architects, to combat the inevitable erroneous and malicious input entered by our users. The WPF binding infrastructure has support for input validation. In the next few sections of this article, I'll examine how to make use of the WPF support for validation, as well as how to display validation error messages to the user.
Input Validation via ValidationRules

The Binding class has a validationRules’ property, which can store any number of ValidationRule-derived classes. Each of those rules can contain some logic that tests to see if the bound value is valid.

WPF only came with one ValidationRule subclass, called ExceptionValidationRule. Developers could add that rule to a binding's ValidationRules and it would catch exceptions thrown during updates made to the data source, allowing the UI to display the exception's error message. The usefulness of this approach to input validation is debatable, considering that the bedrock of good user experience is to avoid unnecessarily revealing technical details to the user. The error messages in data parsing exceptions are generally too technical for most users, but I digress.

Suppose that you have a class that represents an era of time, such as the simple Era class seen here:
public class Era : INotifyPropertyChanged    {                // Declare the event        public event PropertyChangedEventHandler PropertyChanged;        public Era()        {        }        private DateTime m_StartDate;        public DateTime StartDate        {            get { return m_StartDate; }            set            {                m_StartDate = value;                // Call OnPropertyChanged whenever the property is updated                OnPropertyChanged("StartDate");            }        }        private TimeSpan m_Duration;        public TimeSpan Duration         {            get { return m_Duration; }            set            {                m_Duration = value;                // Call OnPropertyChanged whenever the property is updated                OnPropertyChanged("Duration");            }        }        // Create the OnPropertyChanged method to raise the event        protected void OnPropertyChanged(string prop)        {            PropertyChangedEventHandler handler = PropertyChanged;            if (handler != null)            {                handler(this, new PropertyChangedEventArgs(prop));            }        }    }

If you want to allow the user to edit the start date and duration of an era, you could use two textbox controls and bind their Text properties to the properties of an Era instance. Since the user could enter any text he wants into a textbox, you cannot be sure that the input text will be convertible to an instance of DateTime or TimeSpan. In this scenario, you can use the ExceptionValidationRule to report data conversion errors, and then display those conversion errors in the user interface. The XAML listed below demonstrates how to achieve this task.
     <Grid>        <Grid.RowDefinitions>            <RowDefinition />            <RowDefinition />            <RowDefinition />            <RowDefinition />            <RowDefinition />        </Grid.RowDefinitions>        <!-- START DATE -->        <TextBlock Grid.Row="0">Start Date:</TextBlock>        <TextBox Grid.Row="1">            <TextBox.Text>                <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">                    <Binding.ValidationRules>                        <ExceptionValidationRule />                    </Binding.ValidationRules>                </Binding>            </TextBox.Text>        </TextBox>        <!-- DURATION -->        <TextBlock Grid.Row="2">Duration:</TextBlock>        <TextBox Grid.Row="3"                 Text="{Binding Path=Duration, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}"/>                <Button Grid.Row="4" Click="Button_Click">OK</Button>    </Grid>

Those two textboxes demonstrate the two ways that an ExceptionValidationRule can be added to the ValidationRules of a binding in XAML. The Start Date textbox uses the verbose property-element syntax to explicitly add the rule. The Duration textbox uses the shorthand syntax by simply setting the ValidatesOnExceptions property of the binding to true. Both bindings have their UpdateSourceTrigger property set to PropertyChanged so that the input is validated every time the textbox's Text property is given a new value, instead of waiting until the control loses focus.
 
Figure 1 ExceptionValidationRule Displays Validation Errors

 

Displaying Validation Errors
As seen in Figure 1, the Duration textbox contains an invalid value. The string it contains is not convertible to a TimeSpan instance. The textbox's tooltip displays an error message, and a small red error icon appears on the right side of the control. This behavior does not happen automatically, but it is easy to implement and customize.

Render Input Validation Errors to the User

     <Window.Resources>        <!--        The template which renders a TextBox        when it contains invalid data.        -->        <ControlTemplate x:Key="TextBoxErrorTemplate">            <DockPanel>                <Ellipse                    DockPanel.Dock="Right"                     Margin="2,0"                    ToolTip="Contains invalid data"                    Width="10" Height="10"                    >                    <Ellipse.Fill>                        <LinearGradientBrush>                            <GradientStop Color="#11FF1111" Offset="0" />                            <GradientStop Color="#FFFF0000" Offset="1" />                        </LinearGradientBrush>                    </Ellipse.Fill>                </Ellipse>                <!--                This placeholder occupies where the TextBox will appear.                -->                <AdornedElementPlaceholder />            </DockPanel>        </ControlTemplate>        <!--        The Style applied to both TextBox controls in the UI.        -->        <Style TargetType="TextBox">            <Setter Property="Margin" Value="4,4,10,4" />            <Setter                 Property="Validation.ErrorTemplate"                 Value="{StaticResource TextBoxErrorTemplate}"                 />            <Style.Triggers>                <Trigger Property="Validation.HasError" Value="True">                    <Setter Property="ToolTip">                        <Setter.Value>                            <Binding                                 Path="(Validation.Errors)[0].ErrorContent"                                RelativeSource="{x:Static RelativeSource.Self}"                                />                        </Setter.Value>                    </Setter>                </Trigger>            </Style.Triggers>        </Style>    </Window.Resources>

The static Validation class forms a relationship between a control and any validation errors it contains by the use of some attached properties and static methods. You can reference those attached properties in XAML to create markup-only descriptions of how the user interface should present input validation errors to the user. The above XAML is responsible for explaining how to render input errors messages for the two textbox controls in the previous example.

The Style targets all instances of a textbox in the UI. It applies three settings to a textbox. The first Setter affects the textbox's Margin property. The Margin property is set to a value that provides enough space to display the error icon on the right side.
The next Setter in the Style assigns the ControlTemplate used to render the textbox when it contains invalid data. It sets the attached Validation.ErrorTemplate property to the Control­Template declared above the Style. When the Validation class reports that the textbox has one or more validation errors, the textbox renders with that template. This is where the red error icon comes from, as seen inFigure 1.
The Style also contains a Trigger that monitors the attached Validation.HasError property on the textbox. When the Validation class sets the attached HasError property to true for the textbox, the Style's Trigger activates and assigns a tooltip to the textbox. The content of the tooltip is bound to the error message of the exception thrown when attempting to parse the textbox's text into an instance of the source property's data type.

 

Input Validation via IDataErrorInfo
With the introduction of the Microsoft .NET Framework 3.5, WPF support for input validation vastly improved. The Validation­Rule approach is useful for simple applications, but real-world applications deal with the complexity of real-world data and business rules. Encoding business rules into Validation­Rule objects not only ties that code to the WPF platform, but it also does not allow for business logic to exist where it belongs: in business objects!
Many applications have a business layer, where the complexity of processing business rules is contained in a set of business objects. When compiling against the Microsoft .NET Framework 3.5, you can make use of the IDataErrorInfo interface to have WPF ask business objects if they are in a valid state or not. This removes the need to place business logic in objects separate from the business layer, and it allows you to create UI platform-independent business objects. Since the IDataErrorInfo interface has been around for years, this also makes it much easier to reuse business objects from a legacy Windows Forms or ASP.NET application.
Suppose that you need to provide validation for an era beyond just ensuring that the user's text input is convertible to the source property's data type. It might make sense that an era's start date cannot be in the future, since we do not know about eras that have yet to exist. It might also make sense to require that an era last for at least one millisecond.
These types of rules are similar to the generic idea of business logic in that they are both examples of domain rules. It is best to implement domain rules in the objects that store their state: domain objects. The code listed below shows the SmartEra class, which exposes validation error messages via the IData­ErrorInfo interface.

public class SmartEra : Era , IDataErrorInfo    {        #region IDataErrorInfo Members        public string Error        {            get { return null; }        }        public string this[string property]        {            get            {                string msg = null;                switch (property)                {                    case "StartDate":                        if (DateTime.Now < this.StartDate)                            msg = "Start date must be in the past.";                        break;                    case "Duration":                        if (this.Duration.Ticks == 0)                            msg = "An era must have a duration.";                        break;                    default:                        throw new ArgumentException(                            "Unrecognized property: " + property);                }                return msg;            }        }        #endregion // IDataErrorInfo Members    }


 Consuming the validation support of the SmartEra class from a WPF user interface is very simple. The only thing you have to do is tell the bindings that they should honor the IDataErrorInfo interface on the object to which they are bound. You can do this in one of two ways, as below:

<!-- START DATE --><TextBlock Grid.Row="0">Start Date:</TextBlock><TextBox Grid.Row="1">  <TextBox.Text>    <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">      <Binding.ValidationRules>        <ExceptionValidationRule />        <DataErrorValidationRule />      </Binding.ValidationRules>    </Binding>  </TextBox.Text></TextBox><!-- DURATION --><TextBlock Grid.Row="2">Duration:</TextBlock><TextBox   Grid.Row="3"   Text="{Binding          Path=Duration,          UpdateSourceTrigger=PropertyChanged,          ValidatesOnDataErrors=True,         ValidatesOnExceptions=True}"  />
 
Similar to how you can add the ExceptionValidationRule explicitly or implicitly to a binding's ValidationRules collection, you can add the DataErrorValidationRule directly to the ValidationRules of a binding, or you can just set the ValidatesOnDataErrors property to true. Both approaches result in the same net effect; the binding system queries the data source's IDataErrorInfo interface for validation errors.


 

原创粉丝点击