We need to handle actually 3 types of errors.
- Error generated by Binding engine of WPF when we enter String where Int is needed.
Using UpdateSourceExceptionFilter solves this problem.
- Custom UI level validation.
Using our own Interface and following notification pattern like INotifyPropertyChanged solves this problem.
- Custom back-end level validation.
Handling PropertyChanged event in our ViewModel solves this problem.
One by one solutions
Error generated by Binding engine of WPF when we enter String where Int is needed.
<TextBox VerticalAlignment="Stretch" VerticalContentAlignment="Center" Loaded="TextBox_Loaded">
<TextBox.Text>
<Binding Path="ID" UpdateSourceExceptionFilter="ReturnExceptionHandler" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True" ValidatesOnExceptions="True" >
<Binding.ValidationRules>
<CustomValidRule ValidationStep="ConvertedProposedValue"></CustomValidRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
MainWindow.xaml.cs
object ReturnExceptionHandler(object bindingExpression, Exception exception)
{
vm.CanHello = false;
return "This is from the UpdateSourceExceptionFilterCallBack.";
}
- Custom UI level validation
To enable Button respond properly we need to glue 4 things together viz; ViewModel, Button, ValidationRules, and DataGrid’s template column’s textbox. Otherwise ViewModel.CanHello property can’t be set properly thus making RelayCommand of no use.
Right now ValidationRules : CustomValidRule and NegValidRule are not glued to ViewModel. To make them notify ViewModel about their validation result, they need to fire some event.
We will make use of notification pattern which WPF follows using InotifyPropertyChanged.
We will create an interface IViewModelUIRule for UI level validation rules to interact with ViewModel.
ViewModelUIRuleEvent.cs
using System;
namespace BusinessLogic
{
public interface IViewModelUIRule
{
event ViewModelValidationHandler ValidationDone;
}
public delegate void ViewModelValidationHandler(object sender, ViewModelUIValidationEventArgs e);
public class ViewModelUIValidationEventArgs : EventArgs
{
public bool IsValid { get; set; }
public ViewModelUIValidationEventArgs(bool valid) { IsValid = valid; }
}
}
Our validation rules will now implement this interface.
public class CustomValidRule : ValidationRule, IViewModelUIRule
{
bool _isValid = true;
public bool IsValid { get { return _isValid; } set { _isValid = value; } }
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
int? a = value as int?;
ValidationResult result = null;
if (a.HasValue)
{
if (a.Value > 0 && a.Value < 10)
{
_isValid = true;
result = new ValidationResult(true, "");
}
else
{
_isValid = false;
result = new ValidationResult(false, "must be > 0 and < 10 ");
}
}
OnValidationDone();
return result;
}
private void OnValidationDone()
{
if (ValidationDone != null)
ValidationDone(this, new ViewModelUIValidationEventArgs(_isValid));
}
public event ViewModelValidationHandler ValidationDone;
}
///////////////////////////
public class NegValidRule : ValidationRule, IViewModelUIRule
{
bool _isValid = true;
public bool IsValid { get { return _isValid; } set { _isValid = value; } }
ValidationResult result = null;
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
int? a = value as int?;
if (a.HasValue)
{
if (a.Value < 0)
{
_isValid = true;
result = new ValidationResult(true, "");
}
else
{
_isValid = false;
result = new ValidationResult(false, "must be negative ");
}
}
OnValidationDone();
return result;
}
private void OnValidationDone()
{
if (ValidationDone != null)
ValidationDone(this, new ViewModelUIValidationEventArgs(_isValid));
}
public event ViewModelValidationHandler ValidationDone;
}
Now, we need to update our ViewModel class to maintain validation rules collection. And to handle ValidationDone event fired by our custom validation rules.
namespace BusinessLogic
{
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<ValidationRule> _rules;
public ObservableCollection<ValidationRule> Rules { get { return _rules; } }
public ViewModel()
{
_rules = new ObservableCollection<ValidationRule>();
Rules.CollectionChanged += Rules_CollectionChanged;
MyCollection.CollectionChanged += MyCollection_CollectionChanged;
MyCollection.Add(new Class1("Eins", 1));
MyCollection.Add(new Class1("Zwei", 2));
MyCollection.Add(new Class1("Drei", 3));
}
void Rules_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach (var v in e.NewItems)
((IViewModelUIRule)v).ValidationDone += ViewModel_ValidationDone;
}
void ViewModel_ValidationDone(object sender, ViewModelUIValidationEventArgs e)
{
canHello = e.IsValid;
}
void MyCollection_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach (var v in e.NewItems)
((Class1)v).PropertyChanged += ViewModel_PropertyChanged;
}
void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// if all validations runs good here
// canHello = true;
}
……
Now that we have added Rules collection, we need to add our validation rules to it. For this we need to have reference to our validation rules. We are now adding these rules using XAML, so we will use TexBox’s Loaded event for the TextBox binded to ID field to get access to these like so,
<TextBox VerticalAlignment="Stretch" VerticalContentAlignment="Center" Loaded="TextBox_Loaded">
<TextBox.Text>
<Binding Path="ID" UpdateSourceExceptionFilter="ReturnExceptionHandler" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True" ValidatesOnExceptions="True" >
<Binding.ValidationRules>
<b:CustomValidRule ValidationStep="ConvertedProposedValue"></b:CustomValidRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
//////////////////////
private void TextBox_Loaded(object sender, RoutedEventArgs e)
{
Collection<ValidationRule> rules= ((TextBox)sender).GetBindingExpression(TextBox.TextProperty).ParentBinding.ValidationRules;
foreach (ValidationRule rule in rules)
vm.Rules.Add(rule);
}
Custom back-end level validation.
This is done by handling PropertyChanged event of Class1’s objects. See ViewModel.cs listing above.
void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// if all back-end last level validations run good here
// canHello = true;
}
Note : We can use reflection to avoid handling of TextBox Loaded event. So merely adding validation rules to the will do the work.
IDataErrorInfoon your ViewModel? I would this would be easy using that, and the CanExecute parameter of ICommand. Although depending on the type of ICommand you have you may need to requery CanExecuteChanged manually in the PropertyChanged event. - Rachel