Wednesday 7 November 2012

Textbox validation in WPF, yet another way.

In this post I discussed how to do field validation in WPF and this post I will discuss a different way of doing field validation.

I've written a small tool to export solutions from our development environment and check them in to our TFS instance. There is a textbox control that changes the version of the solutions in the server before programmatically exporting the solutions and then checking them in to TFS. The button to do the export and then check in only activates if there are no validation errors.

Here's the xaml for the application (I removed the <Window.Resources> section for clarity) :

The important things to bear in mind are the binding path value for the version TextBox in this case Version, the properties ValidatesOnDataErrors and NotifyOnValidationError and the CommandBinding.
 <Window x:Class="Deployment.ExportSolutions.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:u="clr-namespace:Deployment.ExportSolutions"
         Title="MainWindow" Height="388" Width="673">
     <Window.CommandBindings>
         <CommandBinding  Command="{x:Static u:MainWindow.ExportCommand }"  CanExecute="Export_CanExecute" />
     </Window.CommandBindings>
     <Grid>
         <Button Content="Change Version and Export Solutions" Height="23" HorizontalAlignment="Left" Margin="330,314,0,0" Name="Change" VerticalAlignment="Top" Width="293" Click="Change_Click" KeyboardNavigation.TabIndex="3" Command="{x:Static u:MainWindow.ExportCommand }" />
         <TextBox Height="23" HorizontalAlignment="Left" Margin="69,8,0,0" Name="Version" VerticalAlignment="Top" Width="190" KeyboardNavigation.TabIndex="0"  Validation.Error="Version_Error">
             <TextBox.Text>
                 <Binding Path="Version" ValidatesOnDataErrors="True" NotifyOnValidationError="True" UpdateSourceTrigger="LostFocus"/>
             </TextBox.Text>
         </TextBox>
         <Label Content="Version" Height="28" HorizontalAlignment="Left" Margin="17,8,0,0" Name="label2" VerticalAlignment="Top" />
         <TextBox Height="266" HorizontalAlignment="Left" Margin="12,42,0,0" Name="messageBox" VerticalAlignment="Top" Width="611"  TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" AcceptsReturn="True" Focusable="False" />
         <Label Content="Use Default Credentials" Height="28" HorizontalAlignment="Left" Margin="330,8,0,0" Name="label1" VerticalAlignment="Top" />
         <CheckBox Height="16" HorizontalAlignment="Left" Margin="459,13,0,0" Name="Credentials" VerticalAlignment="Top"  TabIndex="2"/>
     </Grid>
 </Window>
The command binding is used to enable/disable the button to invoke the process, note how the Button has a command defined.
The properties ValidatesOnDataErrors and NotifyOnValidationError relate to error events.
The binding path value is used to read the textbox value by the validating class. 

Here is the validation class. This class implements the IDataErrorInfo interface, which is what really provides the desired functionality. In this case we are just checking that the value matches a regular expression (removed using directives for clarity):
 
 namespace Deployment.ExportSolutions
 {
     class Validator : IDataErrorInfo
     {        
         public string Version { get; set; }
 
         public string Error
         {
             get { throw new NotImplementedException(); }
         }
 
         public string this[string columnName]
         {
             get
             {
                 string result = null;
 
                 if (columnName == "Version")
                 {
                     if (string.IsNullOrEmpty(Version))
                     {
                         result = "Fill in Version";
                     }
                     else if (!RegexValidator(Version, @"^(\d+\.)(\d+\.)(\d+\.)?(\*|\d+)$"))
                     {
                         result = "Only Valid Version Numbers are accepted. e.g. 0.10.1.1";
                     }
                 }

                 return result;
             }
         }
 
         private bool RegexValidator(string Input, string Pattern)
         {
             bool match = Regex.IsMatch(Input, Pattern);
 
             return match;
         }
     }
 }

Finally the main window file (removed all using directives for clarity). The Arguments struct is there to pass multiple arguments to DoWork event, I'm sure there must be a better way of doing this. 
The DoWork event invokes exports.Exports, which does all the work, not shown for clarity. 
The validationErrors variable keeps track of the number of validation errors and only enables the RoutedCommand if the are none, this in turns allows the button to be clicked.
 
 namespace Deployment.ExportSolutions
 {
    
     public partial class MainWindow : Window
     {
         public static RoutedCommand ExportCommand = new RoutedCommand();
 
         private readonly BackgroundWorker export = new BackgroundWorker();
 
         private Validator validator = new Validator();
 
         int validationErrors = 0;
 
         string version;
         bool isChecked;
 
         public MainWindow()
         {
             InitializeComponent();
 
             this.DataContext = validator;
 
             export.DoWork += (sender, args) =>
             {
                 ExportSolution exports = new ExportSolution(this, messageBox);
 
                 Dispatcher.Invoke((Action)(() => version = Version.Text.Trim()));
                 Dispatcher.Invoke((Action)(() => isChecked = (bool)Credentials.IsChecked));
 
                 exports.Export(version, isChecked);
             };
 
             export.RunWorkerCompleted += new RunWorkerCompletedEventHandler(export_RunWorkerCompleted);
         }
 
         private void export_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
         {
             Change.IsEnabled = true;
         }
 
         private void Change_Click(object sender, RoutedEventArgs e)
         {
             Change.IsEnabled = false;
 
             export.RunWorkerAsync();
         }
 
         private void Export_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         {
             e.CanExecute = validationErrors == 0;
             e.Handled = true;
         }
 
         private void Version_Error(object sender, ValidationErrorEventArgs e)
         {
             if (e.Action == ValidationErrorEventAction.Added)
             {
                 validationErrors++;
             }
             else
             {
                 validationErrors--;
             }
         }
     }
 
     public struct Arguments
     {
         public string version;
         public bool isChecked;
 
         public Arguments(string Version, bool IsChecked)
         {
             version = Version;
             isChecked = IsChecked;
         }
     }
 }

No comments:

Post a Comment