Click here to Skip to main content
15,886,873 members
Articles / Desktop Programming / WPF
Article

WPF Input Validation and Sanitization Framework

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
12 Oct 2011MIT7 min read 20K   276   9   4
Overview of a useful framework for doing input validation and sanitization in WPF in an MVVM-friendly way

Introduction

Input validation with WPF and the MVVM design pattern is something of a tricky beast to handle. The default validation syntax available in XAML is not conducive to the separation of concerns that MVVM design patterns encourage. Validation logic doesn’t belong in the View. You should be able to unit test your Models error detection and input validation. If validation is in the View, you cannot do that.

At the time I initially created this solution, a fairly extensive Google search didn’t find any solutions that I was happy with. My understanding is that there are some pretty good options out there now that even use some classes available to you in .NET. So this article may be nothing more than an intellectual exercise, but I have still found the solution I am sharing to be extremely fast to work with, and to include additional capability that I haven’t seen in other solutions.

The project provided is very simple. I leave the ability for you to implement roll back behavior on individual fields, but I didn’t include the SimpleCommand class that I typically use. That is part of my Timeline project. The template in the project has a stubbed out button that can be hooked up to roll back functionality. You can see it in action with my BookWeaver application.

Background

I first developed this solution several years ago while doing some WPF work as a consultant for a currency trading firm. They retained all ownership and rights to that code, so I left that gig with all the concepts, but none of the implementation. I later recreated the concepts with small simplifications. Their implementation included localization, and a few other advanced needs that I haven’t since recreated.

Goals

I set about this project with a few goals:

  • Have each field in the Model know its state including:
    • Changed/Unchanged
    • Valid/Invalid
    • Error Message
  • Provide a simple and reusable means in the View to bind to and respond to the field’s state information.
  • Provide automatic input sanitization such as string trimming.
  • Eliminate tedious and repetitive code in the Model to check for errors. For example, I didn’t want to have if(field <0) fieldError=true… repeated in the Model every time I needed a greater than zero validation rule.
  • Eliminate tedious and repetitive code in the Model to sanitize input.
  • Allow commit and roll back on the Model.
  • Provide the View with information to display a validation summary

As you can probably tell, a lot of these goals have been achieved in different frameworks, etc. I still feel that the project I am sharing can be very useful. This simple implementation doesn’t address the need to localize error messages, but it can be extended to separate the error message from the Model layer of code.

Using the Code

ModelBase

My implementation uses a ModelBase class that uses a generic so you can set a Model to back any Entity. The ModelBase tracks an observable collection of errors and changed fields. It also has event handlers that a specific implementation can tie into to tie into error and change state management in the base. You can bind your View to the ObservableCollections in the Model to display validation summaries, etc.

ExtendedBoundField

ExtendedBoundField class is also a generic class. The class allows you to add to a collection of validators and sanitizers to a given value. It knows the value’s original and current value, and consequently knows the fields state.

Validators and Sanitizers

This project has a simple collection of Validators and Sanitizers you can use. You can also implement field comparison validators and many other kinds with a little bit of creativity. Every Validator or Sanitizer you create can be snapped into any ExtendedBoundField, eliminating repetitive hand coded validation and sanitization.

Comparers and Cloners

Should the need arise, you can control how comparison and assignment of your fields are managed as well. I prefer to keep things simple enough not to need these because they can become serious brain teasers if you are not careful.
Disclaimer: The implementation with Comparers and Cloners hasn’t been thoroughly tested in this implementation. I believe it should work well, but none of my personal projects have needed this. If you do use it, make sure to test it thoroughly.

Model Implementations

This project has a very simple Model backing a dummy Person Entity. The potential downside to the framework at this point is the need for a lot of code in the specific Model. Fortunately, the use of a code snippet allows you to very rapidly generate the code you need for each ExtendedBoundField. If you install the code snippet included in this demo project, then you can declare and hook up an ExtendedBoundField in a matter of seconds. Simply type “exf” and tab through the snippets' different steps.

C#
//Via a snippet, I can create a field in the model that wires to 
//everything I need in about 3 seconds
        #region FirstName Extended Bound Field
        private ExtendedBoundField<string /> _firstName;
        public ExtendedBoundField<string /> FirstName
        {
            get
            {
                return _firstName;
            }
            set
            {
                _firstName = value;
                _firstName.ErrorChangedHandler += Model_ErrorChangedHandler;
                _firstName.ValueChangedHandler += Model_ValueChangedHandler;
                _firstName.PropertyChanged += FirstName_PropertyChanged;
                _firstName.OnCommitted += FirstName_OnCommitted;
            }
        }
        void FirstName_OnCommitted(object sender, EventArgs e)
        {
            MyEntity.FirstName = FirstName.Value;
        }
        void FirstName_PropertyChanged(object sender, 
		System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Value")
           {
                MyEntity.FirstName = FirstName.Value;
            }
        }
        #endregion

Notice that the Class Property for each ExtendedBoundField wires up the events from the field to the appropriate information in the ModelBase in the setter. That allows us to encapsulate most of the work with an ExtendedBoundField so that we can do this in a snippet. The region is particularly nice.

C#
_firstName.ErrorChangedHandler += Model_ErrorChangedHandler;
               _firstName.ValueChangedHandler += Model_ValueChangedHandler;
               _firstName.PropertyChanged += FirstName_PropertyChanged;
               _firstName.OnCommitted += FirstName_OnCommitted;

In your Model’s constructor, you can then initialize each field and assign it validators and sanitizers. The last thing you need to do in the Model is to override Commit and RollBack. You simply call each ExtendeBoundField’s Commit or RollBack functions.

C#
//in your constructor
FirstName = new ExtendedBoundField<string />(p.FirstName, "FirstName", 
		"First Name is required and must be no longer than 100 letters");
            var trimmer = new TrimStringSanitizer();
            FirstName.AddSanitizer(trimmer);
            var val = new RequiredStringValidator(true);
            FirstName.AddValidator(val);
            var max = new MaxLengthValidator(100);
            FirstName.AddValidator(max);

Note: Commit is very important if you want input sanitization to work. The Sanitizers run when a given ExtendedBoundField is committed. So if you never call that, then your Sanitizers do nothing.

If you can’t figure out how to handle some complicated validation for your Model via Validators, you can override a Model function AdditionalValidationPasses. This is useful if you need to do more complicated logic across multiple fields. It is to your advantage to do things through the Validators though because that allows you to simply and automatically indicate what fields have errors in the UI, and every Validator you create becomes a reusable tool. Every bit of logic you put in the override does not give you that benefit.

Similarly, you can override AdditionalHasChangesCheck if you can’t figure out Cloners for your complex types. If you get stuck with a difficult input sanitization, you can do work in the Commit function. As mentioned, it will best suit your needs to leverage the framework, and I have found that you can for the vast majority of the scenarios you will encounter.

ViewModels

This project doesn’t demonstrate how the ViewModels can work with this. ViewModels should expose commands that allow Views to run commands to commit or roll back the Model’s values, among other things. The ViewModelBase class also has several other properties that I usually find helpful in my MVVM development.

Views

The last trick to making this framework useful comes in how you bind to the field to respond to the change and error state in your fields. The key to making this work is understanding how DataContext as a property behaves. By binding a given UI Element’s DataContext to the ExtendedBoundField, you can then create styles and templates that have simple paths to every property on the ExtendedBoundField. All of this would have been for naught without that because you would have to duplicate triggers and styles for the specific path to every ExtendedBoundField.

XML
<TextBox  Style="{StaticResource EBTextBoxStyle}"
                        DockPanel.Dock="Left"
                        DataContext="{Binding Path=MyModel.FirstName}"
                         Width="150"
                         HorizontalAlignment="Left"/>

The following style works for all textboxes with an ExtendedBoundField as the data context.

XML
<Style x:Key="EBTextBoxStyle"
         BasedOn="{StaticResource {x:Type TextBox}}"
         TargetType="{x:Type TextBox}">
      <Style.Triggers>
          <MultiDataTrigger>
              <MultiDataTrigger.Conditions>
                  <Condition Binding="{Binding Path=IsChanged}"
                             Value="true" />
                  <Condition Binding="{Binding Path=IsValid}"
                             Value="false" />
              </MultiDataTrigger.Conditions>
              <Setter Property="BorderBrush"
                      Value="{StaticResource ErrorBrush}" />

              <Setter Property="BorderThickness"
                      Value="2" />
          </MultiDataTrigger>
          <MultiDataTrigger>
              <MultiDataTrigger.Conditions>
                  <Condition Binding="{Binding Path=IsChanged}"
                             Value="true" />
                  <Condition Binding="{Binding Path=IsValid}"
                             Value="true" />
              </MultiDataTrigger.Conditions>
              <Setter Property="BorderBrush"
                      Value="{StaticResource ChangeBrush}" />
              <Setter Property="BorderThickness"
                      Value="2" />

          </MultiDataTrigger>
          <MultiDataTrigger>
              <MultiDataTrigger.Conditions>
                  <Condition Binding="{Binding Path=IsChanged}"
                             Value="false" />
                  <Condition Binding="{Binding Path=IsValid}"
                             Value="true" />
              </MultiDataTrigger.Conditions>

          </MultiDataTrigger>

      </Style.Triggers>
      <Setter Property="Text"
              Value="{Binding Path=Value,
      UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
      <Setter Property="VerticalAlignment"
              Value="Top" />
      <Setter Property="Template">
          <Setter.Value>
              <ControlTemplate TargetType="{x:Type TextBoxBase}">
                  <Grid HorizontalAlignment="Stretch"
                        Width="{TemplateBinding Width}">
                      <Grid.ColumnDefinitions>
                          <ColumnDefinition Width="*" />
                          <ColumnDefinition Width="Auto" />
                      </Grid.ColumnDefinitions>
                      <Border BorderBrush="{TemplateBinding Property=BorderBrush}"
                              BorderThickness=
              "{TemplateBinding Property=BorderThickness}"
                              Grid.Column="0"
                              HorizontalAlignment="Stretch"
                              Background="{TemplateBinding Property=Background}">
                          <ScrollViewer HorizontalAlignment="Stretch"
                                        x:Name="PART_ContentHost" />
                      </Border>
                      <DockPanel Grid.Column="1"
                                 LastChildFill="True">
                          <!-- Command="{Binding Path=RollBackCommand}"
                          In my implementation, I also added a rollback command
           to the field itself
                          so that I could template out a roll back button
                          that would show up if you have changes.
                          I left that in for this, but it isn't hooked up 8)-->
                          <!--Content="{StaticResource RollBackImg}"-->
                          <Button ToolTip="Roll back - not hooked up"
                                  Name="RollBackButton"
                                  IsTabStop="False"
                                  DockPanel.Dock="Left" />
                          <!--This ought to be an image.
                          It gets a tooltip with your error message which is nice-->

                          <ContentPresenter Content="{StaticResource WarningImg}"
                                            ToolTip="{Binding Path=Message}"
                                            Name="ErrorIcon" />
                      </DockPanel>

                  </Grid>
                  <ControlTemplate.Triggers>
                      <DataTrigger Binding="{Binding Path=IsValid}"
                                   Value="true">
                          <Setter TargetName="ErrorIcon"
                                  Property="Visibility"
                                  Value="Collapsed" />
                      </DataTrigger>
                      <DataTrigger Binding="{Binding Path=IsValid}"
                                   Value="false">
                          <Setter TargetName="ErrorIcon"
                                  Property="Visibility"
                                  Value="Visible" />
                      </DataTrigger>

                      <DataTrigger Binding="{Binding Path=IsChanged}"
                                   Value="true">
                          <Setter TargetName="RollBackButton"
                                  Property="Visibility"
                                  Value="Visible" />
                      </DataTrigger>
                      <DataTrigger Binding="{Binding Path=IsChanged}"
                                   Value="false">
                          <Setter TargetName="RollBackButton"
                                  Property="Visibility"
                                  Value="Collapsed" />
                      </DataTrigger>
                  </ControlTemplate.Triggers>
              </ControlTemplate>
          </Setter.Value>
      </Setter>
  </Style>

Conclusion

I have found this implementation to be extremely flexible, extensible, and fast to work with. Granted, if you take away the snippet, it becomes a lot more work to use. I recognize that in some ways this reinvents wheels that exist, though to my knowledge those wheels didn’t exist at the time I created this. This does add a few bits of nice functionality that I am not aware of in any other solution. I hope this is useful for other WPF developers out there. Even if it just primes you with an idea or two for using other Validation methods, then that is cool.

History

  • 10th October, 2011: Initial version 

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionDuplicate!!! Pin
kiran dangar13-Oct-11 0:34
kiran dangar13-Oct-11 0:34 
AnswerRe: Duplicate!!! Pin
DannyStaten13-Oct-11 3:48
DannyStaten13-Oct-11 3:48 
GeneralRe: Duplicate!!! Pin
kiran dangar13-Oct-11 19:13
kiran dangar13-Oct-11 19:13 
AnswerRe: Duplicate!!! Pin
DannyStaten13-Oct-11 11:36
DannyStaten13-Oct-11 11:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.