The new Validation states for controls in Silverlight 5 sure look nice but there are a number of limitations. For starters, you can only invoke them through:
- An exception thrown by a bound property setter
- An exception thrown by a ValueConverter
You can't even use the new ValidationAttributes from System.ComponentModel.DataAnnotations. Well, actually you can but you'd have to this inside the setter:
[Range(18, int.MaxValue, ErrorMessage="Must be 18 or over")]
public int Age
{
get { return _age; }
set
{
Validator.ValidateProperty(value, new ValidationContext(this, null, null)
{
MemberName = "Age",
});
_age = value;
OnPropertyChanged("Age");
}
}
public int Age
{
get { return _age; }
set
{
Validator.ValidateProperty(value, new ValidationContext(this, null, null)
{
MemberName = "Age",
});
_age = value;
OnPropertyChanged("Age");
}
}
What's particularly tricky is to display the invalid state if the value is never changed by the user. For example, you have a name field that is required:
[Reguired(ErrorMessage="Name is required")]
public string NameThis is bound to a TextBox:
<TextBox Text="{Binding Name, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}" x:Name="NameTextBox"/>
<Button Content="Save" x:Name="SaveButton" />The problem is, the user might click save without ever entering the a name. Oh no!
[Reguired(ErrorMessage="Name is required")]
public string NameThis is bound to a TextBox:
<TextBox Text="{Binding Name, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}" x:Name="NameTextBox"/>
<Button Content="Save" x:Name="SaveButton" />The problem is, the user might click save without ever entering the a name. Oh no!
Sure, we can catch that in our code (using the Validator type again, for example). However, there's no easy way of forcing the control to display the error. The only way to achieve this is to force the binding to update programmatically, like so:
BindingExpression binding = NameTextBox.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();Now, to get this working on any scale is going to require code-behind. Lots of code-behind. And everybody knows I hate this. The whole validation story at the moment isn't going to play at all well with Model-View-ViewModel (MVVM).
BindingExpression binding = NameTextBox.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();Now, to get this working on any scale is going to require code-behind. Lots of code-behind. And everybody knows I hate this. The whole validation story at the moment isn't going to play at all well with Model-View-ViewModel (MVVM).
Re-introducing the ValidationScope
In this update - the ValidationScope class becomes much more important and has more to offer than just an attached properties/behavior.
It now becomes an integral part of your ViewModel. Let's walk through a scenario. Here we'll have a ViewModel that exposes a Person property of type Person:
// properties snipped down for brevity
public class Person
{
[Required(ErrorMessage= "Name is required")]
public string Name {}
[Required(ErrorMessage = "Salutation is required")]
public string Salutation {}
[Range(0, int.MaxValue, ErrorMessage = "Must be over 18")]
public int Age {}
}Nice and easy. Now the validation scope comes into play - we add an instance to our ViewModel because we'll access it via binding. To be honest, this could go almost anywhere you like provided it's accessible in a Binding (resources, in the Person class itself, anywhere you like!).
// properties snipped down for brevity
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> Salutations {}
public Person Person {}
public ValidationScope PersonValidationScope {}
}So there's our model and our ViewModel. Now for some view - our Xaml (again, simplified for brevity):
<StackPanel local:ValidationScope.ValidationScope="{Binding PersonValidationScope}">
<TextBox
Text="{Binding Person.Name, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
local:ValidationScope.ValidateBoundProperty="Text" />
<TextBox
Text="{Binding Person.Age, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
local:ValidationScope.ValidateBoundProperty="Text" />
<ComboBox
ItemsSource="{Binding Salutations}"
SelectedItem="{Binding Person.Salutation, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
local:ValidationScope.ValidateBoundProperty="SelectedItem" />
<Button Content="Save" Click="SaveButtonClick" />
</StackPanel>And the code-behind:
//Note - I'd normally use Prism's DelegateCommand and commanding support to avoid this code-behind but don't want to muddy the example
private void SaveButtonClick(object sender, RoutedEventArgs e)
{
_personViewModel.Save();
}Finally, the Save method on the ViewModel
public void Save()
{
// This causes all registered bindings to be updated
PersonValidationScope.ValidateScope();
if (PersonValidationScope.IsValid())
{
// Save changes!
}
}
// properties snipped down for brevity
public class Person
{
[Required(ErrorMessage= "Name is required")]
public string Name {}
[Required(ErrorMessage = "Salutation is required")]
public string Salutation {}
[Range(0, int.MaxValue, ErrorMessage = "Must be over 18")]
public int Age {}
}Nice and easy. Now the validation scope comes into play - we add an instance to our ViewModel because we'll access it via binding. To be honest, this could go almost anywhere you like provided it's accessible in a Binding (resources, in the Person class itself, anywhere you like!).
// properties snipped down for brevity
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> Salutations {}
public Person Person {}
public ValidationScope PersonValidationScope {}
}So there's our model and our ViewModel. Now for some view - our Xaml (again, simplified for brevity):
<StackPanel local:ValidationScope.ValidationScope="{Binding PersonValidationScope}">
<TextBox
Text="{Binding Person.Name, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
local:ValidationScope.ValidateBoundProperty="Text" />
<TextBox
Text="{Binding Person.Age, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
local:ValidationScope.ValidateBoundProperty="Text" />
<ComboBox
ItemsSource="{Binding Salutations}"
SelectedItem="{Binding Person.Salutation, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
local:ValidationScope.ValidateBoundProperty="SelectedItem" />
<Button Content="Save" Click="SaveButtonClick" />
</StackPanel>And the code-behind:
//Note - I'd normally use Prism's DelegateCommand and commanding support to avoid this code-behind but don't want to muddy the example
private void SaveButtonClick(object sender, RoutedEventArgs e)
{
_personViewModel.Save();
}Finally, the Save method on the ViewModel
public void Save()
{
// This causes all registered bindings to be updated
PersonValidationScope.ValidateScope();
if (PersonValidationScope.IsValid())
{
// Save changes!
}
}
How it all works
Whilst getting here took me a whole morning of confusion - it's actually quite straightforward.
First, we use an attached behavior to pass the FrameworkElement we want to be the conceptual 'validation scope' within the VisualTree to our actual ValidationScope instance:
<StackPanel local:ValidationScope.ValidationScope="{Binding PersonValidationScope}">Then we specify the property who is bound and might need a refresh for each control:
<TextBox local:ValidationScope.ValidateBoundProperty="Text" />
<ComboBox local:ValidationScope.ValidateBoundProperty="SelectedItem" />Sadly, this is really a violation of the DRY principle anyway but it does have the added advantage of having to opt in your Bindings to the ValidationScope. Finally, when we're ready, we tell the ValidationScope to update all the bindings:
PersonValidationScope.ValidateScope();This kicks the process into action with a crawl of the VisualTree inside the FrameworkElement registered as our scope (the StackPanel in this case) and hunts out any attached ValidateBoundProperty properties wired to controls. When it finds them it looks for the appropriate DependencyProperty
<StackPanel local:ValidationScope.ValidationScope="{Binding PersonValidationScope}">Then we specify the property who is bound and might need a refresh for each control:
<TextBox local:ValidationScope.ValidateBoundProperty="Text" />
<ComboBox local:ValidationScope.ValidateBoundProperty="SelectedItem" />Sadly, this is really a violation of the DRY principle anyway but it does have the added advantage of having to opt in your Bindings to the ValidationScope. Finally, when we're ready, we tell the ValidationScope to update all the bindings:
PersonValidationScope.ValidateScope();This kicks the process into action with a crawl of the VisualTree inside the FrameworkElement registered as our scope (the StackPanel in this case) and hunts out any attached ValidateBoundProperty properties wired to controls. When it finds them it looks for the appropriate DependencyProperty
No comments:
Post a Comment