Reputation: 200
I have created a sample MVVM application attached.
What I want to achieve is:
I'd like to validate single cells/rows (this is already working, too)
I'd like to do "form" validation. E.g. Cross-row validation to validate that no duplicate strings are in the first column of the datagrid.
How would you do this? I have no clue how to implement the form validation in the view model.
My first idea was just to call a validation method on the viewmodel from the code behind every time anything changes. But doing so, I still don't know how to inform the viewmodel about an validation error in the view's validation rule (e.g. if someone enters text to the ID column). The viewmodel would simply not know about it and eventually validate successfully, just because the wrong value never reaches it. Ok, I could use strings and do the whole conversion in the viewmodel - but I don't like this idea, because I would like to use the whole power of the converters/validators in WPF.
Has anybody already done something like that?
https://www.dropbox.com/s/f3a1naewltbl9yp/DataGridValidationTest.zip?dl=0
Upvotes: 1
Views: 1926
Reputation: 9827
We need to handle actually 3 types of errors.
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.";
}
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.
Upvotes: 1
Reputation: 2781
Kinda brute force approach. I kinda did the following design in my project. It's kinda hard to explain in text so hope you'll understand what I typed here
I would have the following design
CellLevelViewModel
With the above design setup, all you need is having a EventHandler at your ViewModelBase that fired after an ViewModel performed Validation. Use this event to Trigger parent ViewModel to perform it's own level of validation and keep populate the error result back to the root view model.
Upvotes: 0
Reputation: 12846
I dont believe it is possible to validate a row using multiple columns in a DataGrid
. But, as you mentioned, you can do it using the viewmodel.
You would have to store the rows of the DataGrid
in the ViewModel
(but I expect you are doing that already). The you need to implement INotifyDataErrorInfo
. This interface allows you to notify the view if some errors changed.
Then, every time the name
property is changed, validate if there are any duplicates.
Your save button should use an ICommand
to invoke the save action. In the CanExecute
method you can check the HasErrors
property of the object that implements INotifyDataErrorInfo
and return the appropriate boolean
. This disables the button accordingly.
Upvotes: 0