Thursday, July 7, 2011

Taking data binding, validation and MVVM to the next level - part 1

I've been having fun today, working on something that on the surface seems very simple, but once you delve into it there are a lot of complexities and edge cases hidden just below the surface.

Today boys and girls, let's talk about how to validate a file system path. We are going to do this in a nice MVVM compliant way.

First, let us set the scene; how many times have you coded up a window with a textbox and a simple button which opens the file or folder browse dialog:



The XAML code for this is very simple:
<Window x:Class="FilePathValidation1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
SizeToContent="WidthAndHeight"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="20" >
<TextBlock Text="Enter the path to your file" VerticalAlignment="Bottom" />
<TextBox x:Name="FilePathTextBox" Text="{Binding FilePath}" Width="350" Margin="5,0,0,0" />
<Button x:Name="FileBrowseButton"
Content="..."
Command="{Binding FileBrowseCommand}"
Width="20" Margin="5,0,0,0"
/>
</StackPanel>
</Grid>
</Window>


And the class behind:

using System;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using System.ComponentModel;

namespace FilePathValidation1
{

///
/// Interaction logic for MainWindow.xaml
///

public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(MainWindowLoaded);
}

private void MainWindowLoaded(object sender, RoutedEventArgs e)
{
this.DataContext = this;
}

///
/// Gets the command used to browse for a file.
///

public ICommand FileBrowseCommand
{
get
{
if (_fileBrowseCommand == null)
_fileBrowseCommand = new RelayCommand(OpenFileBrowseDialog);
return _fileBrowseCommand;
}
}

///
/// Gets and sets the file path.
///

public string FilePath
{
get { return _filePath; }
set
{
if (!string.Equals(value, _filePath, StringComparison.InvariantCultureIgnoreCase))
{
_filePath = value;
OnPropertyChanged("FilePath");
}
}
}

private void OpenFileBrowseDialog(object context)
{
OpenFileDialog dlg = new OpenFileDialog();
var retVal = dlg.ShowDialog();
if (retVal.HasValue && retVal.Value)
{
FilePath = dlg.FileName;
}
}

///
/// Raises the event.
///

/// The name of the property that changed.
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}

///
/// Occurs when a property value changes.
///

public event PropertyChangedEventHandler PropertyChanged;


private ICommand _fileBrowseCommand;
private string _filePath;
}
}


Just remember this code isn't designed to win any awards for being pretty.
So if you have a play with the code above, you'll find that you can either enter a file path directly in the textbox, or you can pop open the file browse dialog and select an existing file. All good, yeah?

But pretty quickly you'll also discover that you need to validate anything the user enters. To do this, we'll take advantage of the ValidationRule class that is already built into the framework, and the ValidationRules property that is built into the WPF binding mechanism.

Let's start with the validation rule:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;

namespace FilePathValidation1
{
public class FilePathValidationRule : ValidationRule
{

public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value != null && value.GetType() != typeof(string))
return new ValidationResult(false, "Input value was of the wrong type, expected a string");

var filePath = value as string;

if (string.IsNullOrWhiteSpace(filePath))
return new ValidationResult(false, "The file path cannot be empty or whitespace.");


return new ValidationResult(true, null);
}
}
}


This validation rule simply extends the System.Windows.Controls.ValidationRule class that is found in the PresentationFramework assembly, we've got just a couple of simple checks in it for now.

Here is how we use it in the XAML:
<Window x:Class="FilePathValidation1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
SizeToContent="WidthAndHeight"

xmlns:this="clr-namespace:FilePathValidation1"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="20" >
<TextBlock Text="Enter the path to your file" VerticalAlignment="Bottom" />
<TextBox x:Name="FilePathTextBox" Width="350" Margin="5,0,0,0">
<TextBox.Text>
<Binding Path="FilePath" UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<this:FilePathValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>

</TextBox>
<Button x:Name="FileBrowseButton"
Content="..."
Command="{Binding FileBrowseCommand}"
Width="20" Margin="5,0,0,0"
/>
</StackPanel>
</Grid>
</Window>


I have highlighted the differences in yellow. Notice how we are now using the long form for specifying the binding on the textbox, and we can also specify any number of validation rules to run. These rules are run whenever the user changes what is in the textbox, this is controlled by the UpdateSourceTrigger property on the binding (for example, I could change it so the validation only runs when the user removes the focus from the textbox).

Soooo... all you have to do now to see this in action is run the project, enter some text in the textbox, then delete the text and whammo!! the validation rule will return a validation error, the border of the TextBox will turn red, and with the addition of a handy style on the TextBox the validation message will show in the tooltip:



Pretty nifty, yeah?
Okay, this post is long enough and it lays down the base of what we are going to continue and work with. You need to validate more than just a path consisting of whitespace, you need to also check for forbidden characters and stuff. Go and make yourself a coffee, then come back and read part 2, where I show you why validating a file path can be so darned tricky.

No comments: