How do you decouple your ViewModel properties validation from ViewModel? - c#

I am using MVVMLight. This is my Department model/POCO class. I do not want to pollute it by any means.
public partial class Department
{
public int DepartmentId { get; set; }
public string DepartmentCode { get; set; }
public string DepartmentFullName { get; set; }
}
Here is the CreateDepartmentViewModel :
public class CreateDepartmentViewModel : ViewModelBase
{
private IDepartmentService departmentService;
public RelayCommand CreateDepartmentCommand { get; private set; }
public CreateDepartmentViewModel(IDepartmentService DepartmentService)
{
departmentService = DepartmentService;
this.CreateDepartmentCommand = new RelayCommand(CreateDepartment, CanExecute);
}
private Department _department = new Department();
public Department Department
{
get
{
return _department;
}
set
{
if (_department == value)
{
return;
}
_department = value;
RaisePropertyChanged("Department");
}
}
private Boolean CanExecute()
{
return true;
}
private void CreateDepartment()
{
bool success = departmentService.SaveDepartment(_department);
}
}
The DepartmentCode and DepartmentFullName is bind to UI as shown below.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Department Code" Grid.Row="0"/>
<TextBox Grid.Row="0" Text="{Binding Department.DepartmentCode, Mode=TwoWay}" Margin="150,0,0,0"/>
<TextBlock Text="Department Name" Grid.Row="1"/>
<TextBox Grid.Row="1" Text="{Binding Department.DepartmentFullName, Mode=TwoWay}" ToolTip="Hi" Margin="150,0,0,0"/>
<Button Grid.Row="2" Content="Save" Width="50" Command="{Binding CreateDepartmentCommand}"/>
</Grid>
Before saving the Department, I need to validate that both DepartmentCode and DepartmentFullName has some text in it.
Where should my validation logic reside ? In ViewModel itself ? If so, how do i decouple my validation logic so that it is also unit testable ?

I've found the easiest way to accomplish this is to use a
System.Windows.Controls.ValidationRule
It only takes 3 straight-forward steps.
First you create a ValidationRule. This is a completely separate class that exists outside both your Model and ViewModel and defines how the Text data should be validated. In this case a simple String.IsNullOrWhiteSpace check.
public class DepartmentValidationRule : System.Windows.Controls.ValidationRule
{
public override System.Windows.Controls.ValidationResult Validate(object value, CultureInfo ultureInfo)
{
if (String.IsNullOrWhiteSpace(value as string))
{
return new System.Windows.Controls.ValidationResult(false, "The value is not a valid");
}
else
{
return new System.Windows.Controls.ValidationResult(true, null);
}
}
}
Next, specify that your TextBoxes should use an instance of your new class to perform validation on the Text entered by specifing the ValidationRules property of the Text binding. You get the added bonus of the TextBox border turning red if the Validation fails.
<TextBlock Text="Department Code" Grid.Row="0"/>
<TextBox Name="DepartmentCodeTextBox" Grid.Row="0" Margin="150,0,0,0">
<TextBox.Text>
<Binding Path="Department.DepartmentCode" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:DepartmentValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Text="Department Name" Grid.Row="1"/>
<TextBox Name="DepartmentNameTextBox" Grid.Row="1" ToolTip="Hi" Margin="150,0,0,0">
<TextBox.Text>
<Binding Path="Department.DepartmentFullName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:DepartmentValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Finally, create a Style to disable the Save button if either TextBox fails validation. We do this by binding to the Validation.HasError property of the Textbox we bound our Validation rule to. We'll name this style DisableOnValidationError just to make things obvious.
<Grid.Resources>
<Style x:Key="DisableOnValidationError" TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=DepartmentCodeTextBox}" Value="True" >
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=DepartmentNameTextBox}" Value="True" >
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
And finally we set the DisableOnValidationError style on the Save button
<Button Grid.Row="2" Content="Save" Width="50" Command="{Binding CreateDepartmentCommand}"
Style="{StaticResource DisableOnValidationError}"/>
Now, if either of your TextBoxes fails Validation the TextBox gets highlighted and the Save button will be disabled.
The DepartmentValidationRule is completely separate from your business logic and is reusable and testable.

What about using ValidationRules class , this will decouple your model from poppluting it with validation code.
This will work great for individual controls but you can also delegate this logic to some custom validation classes , MvvmValidator framework will help you. This framework lets you write complex validation logic in the form of rules and these rules can be configured at ViewModel level and can be fired on submit button. its a nice decouple way of applying validations without populating your domian objects.

Create a DepartmentValidator class, which will be easily unit tested. Also, this class will allow you to eliminate duplication of validation in the server-side and UI scenarios.
public class DepartmentValidator
{
private class PropertyNames
{
public const string DepartmentFullName = "DepartmentFullName";
public const string DepartmentCode = "DepartmentCode";
}
public IList<ValidationError> Validate(Department department)
{
var errors = new List<ValidationError>();
if(string.IsNullOrWhiteSpace(department.DepartmentCode))
{
errors.Add(new ValidationError { ErrorDescription = "Department code must be specified.", Property = PropertyNames.DepartmentCode});
}
if(string.IsNullOrWhiteSpace(department.DepartmentFullName))
{
errors.Add(new ValidationError { ErrorDescription = "Department name must be specified.", Property = PropertyNames.DepartmentFullName});
}
if (errors.Count > 0)
{
return errors;
}
return null;
}
}
Create a DepartmentViewModel that wraps your Department model and implements IDataErrorInfo, so that you have more granular control and can display validation errors using standard Validation Templates.
public class DepartmentViewModel : IDataErrorInfo, INotifyPropertyChanged
{
private Department _model;
public DepartmentViewModel(Department model)
{
_model = model;
Validator = new DepartmentValidator();
}
public DepartmentValidator Validator { get; set; }
public string DepartmentFullName
{
get
{
return _model.DepartmentFullName;
}
set
{
if(_model.DepartmentFullName != value)
{
_model.DepartmentFullName = value;
this.OnPropertyChanged("DepartmentFullName");
}
}
}
public string DepartmentCode
{
get
{
return _model.DepartmentCode;
}
set
{
if(_model.DepartmentCode != value)
{
_model.DepartmentCode = value;
this.OnPropertyChanged("DepartmentCode");
}
}
}
public int DepartmentId
{
get
{
return _model.DepartmentId;
}
}
public string this[string columnName]
{
get
{
var errors = Validator.Validate(_model) ?? new List<ValidationError>();
if (errors.Any(p => p.Property == columnName))
{
return string.Join(Environment.NewLine, errors.Where(p => p.Property == columnName).Select(p => p.ErrorDescription));
}
return null;
}
}
public string Error
{
get
{
var errors = Validator.Validate(_model) ?? new List<ValidationError>();
return string.Join(Environment.NewLine, errors);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Expose the DepartmentViewModel, rather than the Department Model, and hook up the PropertyChanged event to the CreateDepartmentCommand so that your Save button will be automatically disabled when the department fails validation and so that you can display validation errors. Expose a ValidationErrors property.
public CreateDepartmentViewModel(IDepartmentService DepartmentService)
{
departmentService = DepartmentService;
_department = new DepartmentViewModel(new Department());
this.CreateDepartmentCommand = new RelayCommand(CreateDepartment, CanExecute);
_department.PropertyChanged += (s,a) =>
{
ValidationErrors = Department.Errors;
RaisePropertyChanged("ValidationErrors");
this.CreateDepartmentCommand.RaiseCanExecuteChanged();
}
}
public DepartmentViewModel Department
{
get
{
return _department;
}
set
{
if (_department == value)
{
return;
}
_department = value;
RaisePropertyChanged("Department");
}
}
public string ValidationErrors {get; set;}
private Boolean CanExecute()
{
return string.IsNullOrEmpty(ValidationErrors);
}
Before saving the Department, you might want to validate again.
private void CreateDepartment()
{
if(Department.Error!=null)
{
ValidationErrors = Department.Error;
RaisePropertyChanged("validationErrors");
return;
}
bool success = departmentService.SaveDepartment(_department);
}

I also find this annoying as it drives you business logic into the ViewModel forcing you to accept that and leave it there or duplicate it in the Service Layer or Data Model. If you don't mind losing some of the advantages of using annotations, etc. This is an approach I have used and seen most recommended - adding errors to a ValidationDictionary from the service layer.
You can also mix these, with business logic handled as above in your service layer, and UI-only relevant validations annotated in your ViewModel.
*Note That I am answering this from a MVC perspective, but I think it is all still relevant.

Add new method in your view model (Is Valid) and Modify the CanExecte method, you can easily test this by testing the CanExecute method:
public class CreateDepartmentViewModel : ViewModelBase
{
private IDepartmentService departmentService;
public RelayCommand CreateDepartmentCommand { get; private set; }
public CreateDepartmentViewModel(IDepartmentService DepartmentService)
{
departmentService = DepartmentService;
this.CreateDepartmentCommand = new RelayCommand(CreateDepartment, CanExecute);
}
private Department _department = new Department();
public Department Department
{
get
{
return _department;
}
set
{
if (_department == value)
{
return;
}
_department = value;
RaisePropertyChanged("Department");
}
}
private bool IsValid()
{
return !string.IsNullOrEmpty(this.Department.DepartmentCode) && !string.IsNullOrEmpty(this.Department.DepartmentFullName);
}
private Boolean CanExecute()
{
return this.IsValid();
}
private void CreateDepartment()
{
bool success = departmentService.SaveDepartment(_department);
}
}

You can make your Model class implement IDataErrorInfo interface.
If you don't want to pollute your Model, you can create a new class the inherits from it, and do the validation there
public class ValidDepartment : Department, IDataErrorInfo
{
#region IDataErrorInfo Members
public string Error
{
get { return null; }
}
public string this[string name]
{
get
{
if (name == "DepartmentCode")
{
if (string.IsNullOrEmpty(DepartmentCode)
return "DepartmentCode can not be empty";
}
if (name == "DepartmentFullName")
{
if (string.IsNullOrEmpty(DepartmentFullName)
return "DepartmentFullName can not be empty";
}
return null;
}
}
#endregion
}
In your ViewModel replace Department with ValidDepartment
private ValidDepartment _department = new ValidDepartment ();
public ValidDepartment Department
{
get
{
return _department;
}
set
{
if (_department == value)
{
return;
}
_department = value;
RaisePropertyChanged("Department");
}
}
In your View set ValidatesOnDataErrors=True to your binding controls
<TextBox Grid.Row="1" ToolTip="Hi" Margin="150,0,0,0">
<TextBox.Text>
<Binding Path="Department.DepartmentFullName"
Mode="TwoWay"
ValidatesOnDataErrors="True">
</Binding>
</TextBox.Text>
</TextBox>
Set TextBox Style and Validation.ErrorTemplate to determine how your validation will appear in the UI, for example, via Tooltip :
<Style x:Key="textBoxInError" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
You can learn more about validation in WPF here, and here
Hope this helps

I use fluent validation in all my projects, not only to decouple, but also easily unit test my validation rules. http://fluentvalidation.codeplex.com/.
It also has a nuget package http://www.nuget.org/packages/FluentValidation/

Related

Dotnet Maui DataTrigger not fired on Custom Control Binding

I have created a custom control that is a ValidatableEntry. It has an IsValid public property (bool).
I would like to use this property to Enable/Disable a Button. For this, I think I should be able to use a DataTrigger. However it is not working. The Trigger does not fire when the IsValid property changes.
Here is a simplified version that ilustrates the problem. When the entered text is over 5 characters long, the IsValid property changes to true. However, the trigger is not fired and the button remains disabled.
An example repo can be found here: https://github.com/jokogarcia/ExampleForSO
Custom control:
public class ValidatableEntry : ContentView
{
public Entry Entry { get; set; } = new();
public int MinimumLength { get; set; }
public bool IsValid { get; set; }
public ValidatableEntry()
{
this.Entry.TextChanged += OnTextChanged;
Content = new VerticalStackLayout
{
Children = {
Entry
}
};
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
Entry entry = sender as Entry;
IsValid = entry?.Text?.Length> MinimumLength;
}
}
XAML:
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<local:ValidatableEntry
x:Name="MyEntry"
MinimumLength="5"/>
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
IsEnabled="False"
HorizontalOptions="Center" >
<Button.Triggers>
<DataTrigger TargetType="Button"
Binding="{Binding Source={x:Reference MyEntry},
Path=IsValid}"
Value="True">
<Setter Property="IsEnabled" Value="True"></Setter>
</DataTrigger>
</Button.Triggers>
</Button>
</VerticalStackLayout>
I found my own answer. I'll share it here for others that come after.
What I was missing was to implement INotifyPropertyChanged in my Custom Control. Like this:
public class ValidatableEntry : ContentView, INotifyPropertyChanged
{
[...]
public bool IsValid
{
get { return isValid; }
set
{
isValid = value;
NotifyPropertyChanged();
}
}
[...]
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
EDIT: Actually, this approach later gave me problems when using DataBindings on my control. It is actually not necessary to implement INotifyPropertyChanged because ContentView already implements it. All I needed to do is call OnPropertyChanged() after updating the value.
So the better and simpler answer would be:
public class ValidatableEntry : ContentView
{
[...]
public bool IsValid
{
get { return isValid; }
set
{
isValid = value;
OnPropertyChanged();
}
}

How to pass validation error to child control in reusable UserControl

I've created my own UserControl, called PersonNameControl which is intented to be reused.
The control has three TextBox fields, and has three dependency properties in its class file.
Firstname
Insertion
Lastname
Each dependency property value is bound to to a field, so the dependency property Firstname is bound to the Firstname TextBox, and so on.
I conciously didn't explicitly set the DataContext of the UserControl.
The control should be as loosely as possible. It should only get it's values (for the fields) via its dependency properties. It shouldn't even be looking to anything like DataContext.
<UserControl x:Class="WpfApplication1.PersonNameControl">
<StackPanel>
<Label>Firstname:</Label>
<TextBox Text="{Binding Firstname, Mode=TwoWay,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
</TextBox>
<Label>Insertion:</Label>
<TextBox Text="{Binding Insertion, Mode=TwoWay,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
</TextBox>
<Label>Lastname:</Label>
<TextBox Text="{Binding Lastname, Mode=TwoWay,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
</TextBox>
</StackPanel>
</UserControl>
And the control class:
public partial class PersonNameControl : UserControl
{
public PersonNameControl()
{
InitializeComponent();
}
public string Firstname
{
get { return (string)GetValue(FirstnameProperty); }
set { SetValue(FirstnameProperty, value); }
}
public static readonly DependencyProperty FirstnameProperty =
DependencyProperty.Register("Firstname", typeof(string), typeof(PersonNameControl),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public string Insertion
{
get { return (string)GetValue(InsertionProperty); }
set { SetValue(InsertionProperty, value); }
}
public static readonly DependencyProperty InsertionProperty =
DependencyProperty.Register("Insertion", typeof(string), typeof(PersonNameControl),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public string Lastname
{
get { return (string)GetValue(LastnameProperty); }
set { SetValue(LastnameProperty, value); }
}
public static readonly DependencyProperty LastnameProperty =
DependencyProperty.Register("Lastname", typeof(string), typeof(PersonNameControl),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}
The control is supposed to be used inside another view as follows:
<!--
Here we are inside a view or some other control.
The bindings here provide the dependency properties of the UserControl with a value.
The DataContext of the view where my UserControl is used, is a ViewModel that implements INotifyDataErrorInfo
-->
<myControls:PersonNameControl
Firstname="{Binding SomeFirstnameFromVM, Mode=TwoWay}"
Insertion="{Binding SomeInsertionFromVM, Mode=TwoWay}"
Lastname="{Binding SomeLastnameFromVM, Mode=TwoWay}">
</myControls:PersonNameControl>
When the ViewModel (implements INotifyDataErrorInfo) created a Validation error, nothing happens with my PersonNameControl UserControl.
I managed in making a control that is independent because it doesn't rely on a specific DataContext, doesn't set its own DataContext in its codebehind file, and just gets its values via dependency properties. The values are exchanged via the bindings and show up, but the validation errors don't show.
What I want is passing the validation errors through, to the UserControl.
Some solutions on the internet make use of ValidationAdornerSite and I tried this. But this would only work for one TextBox.
I don't see any solution without making my control dependent on the outside world or introducing ugly extra properties to solve it cumbersome. I thought the errors are 'tunneled' like a piece of information through all bindings towards the last level where the value arrives. But this seems not to be the right consideration.
Edit:
I added my ViewModel class.
public class CustomerFormViewModel : ViewModelBase, INotifyDataErrorInfo
{
protected string _clientNumber;
protected DateTime _date;
protected string _firstname;
protected string _insertion;
protected string _lastname;
protected Address _address;
protected ObservableCollection<Email> _emails;
protected ObservableCollection<PhoneNumber> _phoneNumbers;
protected string _note;
protected bool _hasErrors;
protected IList<ValidationFailure> _validationErrors;
public IList<ValidationFailure> ValidationErrors
{
get { return _validationErrors; }
set { _validationErrors = value; OnPropertyChanged("ValidationErrors"); }
}
public string ClientNumber
{
get { return _clientNumber; }
set { _clientNumber = value; OnPropertyChanged("ClientNumber"); }
}
public DateTime Date
{
get { return _date; }
set { _date = value; OnPropertyChanged("Date"); }
}
public string Firstname
{
get { return _firstname; }
set { _firstname = value; OnPropertyChanged("Firstname"); }
}
public string Insertion
{
get { return _insertion; }
set { _insertion = value; OnPropertyChanged("Insertion"); }
}
public string Lastname
{
get { return _lastname; }
set { _lastname = value; OnPropertyChanged("Lastname"); }
}
public Address Address
{
get { return _address; }
set { _address = value; OnPropertyChanged("Address"); }
}
public ObservableCollection<Email> Emails
{
get { return _emails; }
set { _emails = value; OnPropertyChanged("Emails"); }
}
public ObservableCollection<PhoneNumber> PhoneNumbers
{
get { return _phoneNumbers; }
set { _phoneNumbers = value; OnPropertyChanged("PhoneNumbers"); }
}
public string Note
{
get { return _note; }
set { _note = value; OnPropertyChanged("Note"); }
}
private DelegateCommand _saveCustomerCommand;
public DelegateCommand SaveCustomerCommand
{
get { return _saveCustomerCommand; }
private set { _saveCustomerCommand = value; OnPropertyChanged("SaveCustomerCommand"); }
}
public CustomerFormViewModel()
{
ValidationErrors = new List<ValidationFailure>();
SaveCustomerCommand = new DelegateCommand(SaveCustomer, CanSaveCustomer);
}
protected void ValidateInput()
{
ValidationErrors.Clear();
CustomerFormValidator validator = new CustomerFormValidator();
FluentValidation.Results.ValidationResult result = validator.Validate(this);
ValidationErrors = result.Errors;
foreach (ValidationFailure f in ValidationErrors)
{
Console.WriteLine(f.ErrorMessage);
}
_hasErrors = result.Errors.Count != 0;
List<string> vmProperties = new List<string>() { "Firstname", "Lastname", "Address", "ClientNumber", "Date" };
foreach (string propertyName in vmProperties)
{
OnErrorsChanged(propertyName);
}
}
public bool HasErrors
{
get { return _hasErrors; }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
protected void OnErrorsChanged(string name)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(name));
}
public IEnumerable GetErrors(string propertyName)
{
return ValidationErrors.Where<ValidationFailure>(x => x.PropertyName == propertyName);
}
public void SaveCustomer(object parameter)
{
this.ValidateInput();
if( ! HasErrors)
{
Customer customer = new Customer(-1, ClientNumber, Date, Firstname, Insertion, Lastname, Address);
ICustomerRepository repo = new CustomerRepository();
bool res = repo.SaveCustomer(customer);
if(res) {
// ...
}
// ...
} else
{
MessageBox.Show("One or more fields are not filled in correctly.", "Invalid input", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public bool CanSaveCustomer(object parameter)
{
return true;
}
}
So, I have prepared a demo user control. It is a sub user control, gets all validation info from its MainViewModel
MainWindow
<Window
x:Class="ValidationSubUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ValidationSubUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Name="MyWindow"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<local:SubUserControl
FirstName="{Binding FirstName, Mode=TwoWay}"
LastName="{Binding LastName, Mode=TwoWay}"
ValidationSource="{Binding ElementName=MyWindow, Path=DataContext}" />
</Grid>
</Window>
MainViewModel
using GalaSoft.MvvmLight;
using System.ComponentModel;
namespace ValidationSubUI
{
public class MainViewModel : ViewModelBase, IDataErrorInfo
{
public string Error
{
get
{
return string.Empty;
}
}
private string m_FirstName;
public string FirstName
{
get { return m_FirstName; }
set
{
m_FirstName = value;
RaisePropertyChanged();
}
}
private string m_LastName;
public string LastName
{
get { return m_LastName; }
set
{
m_LastName = value;
RaisePropertyChanged();
}
}
public string this[string columnName]
{
get
{
if (columnName == nameof(FirstName))
{
return GetFirstNameError();
}
else if (columnName == nameof(LastName))
{
return GetLastNameError();
}
return null;
}
}
private string GetFirstNameError()
{
string result = string.Empty;
if (string.IsNullOrEmpty(FirstName))
{
result = "First name required";
}
return result;
}
private string GetLastNameError()
{
string result = string.Empty;
if (string.IsNullOrEmpty(LastName))
{
result = "Last name required";
}
return result;
}
}
}
SubUserControl gets all validation logic from MainViewModel
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace ValidationSubUI
{
/// <summary>
/// Interaction logic for SubUserControl.xaml
/// </summary>
public partial class SubUserControl : UserControl, IDataErrorInfo
{
public SubUserControl()
{
InitializeComponent();
}
public IDataErrorInfo ValidationSource
{
get { return (IDataErrorInfo)GetValue(ValidationSourceProperty); }
set { SetValue(ValidationSourceProperty, value); }
}
// Using a DependencyProperty as the backing store for ValidationSource. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ValidationSourceProperty =
DependencyProperty.Register("ValidationSource", typeof(IDataErrorInfo), typeof(SubUserControl), new PropertyMetadata(null));
public string FirstName
{
get { return (string)GetValue(FirstNameProperty); }
set { SetValue(FirstNameProperty, value); }
}
// Using a DependencyProperty as the backing store for FirstName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty FirstNameProperty =
DependencyProperty.Register("FirstName", typeof(string), typeof(SubUserControl), new PropertyMetadata(string.Empty));
public string LastName
{
get { return (string)GetValue(LastNameProperty); }
set { SetValue(LastNameProperty, value); }
}
// Using a DependencyProperty as the backing store for LastName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LastNameProperty =
DependencyProperty.Register("LastName", typeof(string), typeof(SubUserControl), new PropertyMetadata(string.Empty));
public string Error
{
get
{
return string.Empty;
}
}
public string this[string columnName]
{
get
{
if (ValidationSource != null)
{
return ValidationSource[columnName];
}
return null;
}
}
}
}
and SubUserControl
<UserControl
x:Class="ValidationSubUI.SubUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="CustomControl"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel>
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder x:Name="controlWithError" />
</Border>
<TextBlock
Margin="5,0,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="12"
FontWeight="DemiBold"
Foreground="Red"
Text="{Binding ElementName=controlWithError, Path=AdornedElement.ToolTip, Mode=OneWay}" />
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid DataContext="{x:Reference Name=CustomControl}">
<StackPanel>
<TextBox
Width="120"
Height="30"
Margin="5"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Text="{Binding FirstName, Mode=TwoWay, ValidatesOnDataErrors=True}"
TextWrapping="Wrap" />
<TextBox
Width="120"
Height="30"
Margin="5"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Text="{Binding LastName, Mode=TwoWay, ValidatesOnDataErrors=True}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</UserControl>

Show validation text in separate control

I have a property, bound to a TextBox, which is only allowed to be a positive number (i.e. x > 0). Inspired by this post I have decided to implement this using IDataErrorInfo interface.
Following the instructions in that post, I can get a tool-tip warning to show, if the input cannot be validated. But I would like to have the validation warning shown in a seperate TextBlock, just below the input TextBox.
XAML:
<!-- ValidatingControl Style -->
<Style TargetType="{x:Type FrameworkElement}" x:Key="ValidatingControl">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding
Path=(Validation.Errors)[0].ErrorContent,
RelativeSource={x:Static RelativeSource.Self}}" />
</Trigger>
</Style.Triggers>
</Style>
(...)
<TextBlock Text="Product price:" />
<TextBox
Name="Price"
DataContext="{Binding SelectedProduct}"
Style="{StaticResource ValidatingControl}"
Text="{Binding Path=Price,
StringFormat=N0,
ConverterCulture=da-DK,
Mode=TwoWay,
ValidatesOnDataErrors=True}"/>
<!-- This should contain validation warning -->
<TextBlock Margin="0 0 0 10" />
Binding property (C#):
public class ProductModel : IDataErrorInfo
{
public decimal Price { get; set; }
(...)
// Implementation of IDataErrorInfo
string IDataErrorInfo.Error
{
get { return null; }
}
string IDataErrorInfo.this[string columnName]
{
get
{
if (columnName == "Price")
{
// Validate property and return a string if there is an error
if (Price < 0)
return "Cannot be negative.";
}
// If there's no error, null gets returned
return null;
}
}
}
You can bind your textblock to IDataError interfaces indexer property.
Here modified code
<StackPanel>
<TextBlock Text="Product price:" />
<TextBox Name="Price" Text="{Binding Path=Price, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, ValidatesOnDataErrors=True}"/>
<!-- This should contain validation warning -->
<TextBlock Margin="0 0 0 10" Text="{Binding [Price]}" />
</StackPanel>
Also you need to do some modifications to your view model.
class ViewModel : IDataErrorInfo, INotifyPropertyChanged
{
decimal _price;
public decimal Price
{
get => _price;
set
{
_price = value;
RaisePropertyChanged(nameof(Price));
RaisePropertyChanged("Item[]");
}
}
// Implementation of IDataErrorInfo
string IDataErrorInfo.Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
if (columnName == "Price")
{
// Validate property and return a string if there is an error
if (Price < 0)
return "Cannot be negative.";
}
// If there's no error, null gets returned
return null;
}
}
void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Note, that indexer property for IDataErrorInfo interface is implemented implicitly, otherwise WPF system for some reason can't bind to it.
Also take a look to the INotifyPropertyChanged interface implementation for Price property, especially on the RaisePropertyChanged("Item[]"); line. Without this line WPF's system binding system will not know, if there is an error.
You need to define the indexer as public property, implement the INotifyPropertyChanged interface and raise a change notification for the indexer when the Price property is set. This should work:
public class ProductModel : IDataErrorInfo, INotifyPropertyChanged
{
private decimal _price;
public decimal Price
{
get { return _price; }
set
{
_price = value;
NotifyPropertyChanged();
NotifyPropertyChanged("Item[]");
}
}
string IDataErrorInfo.Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
if (columnName == "Price")
{
// Validate property and return a string if there is an error
if (Price < 0)
return "Cannot be negative.";
}
// If there's no error, null gets returned
return null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<!-- This should contain validation warning -->
<TextBlock DataContext="{Binding SelectedProduct}" Margin="0 0 0 10" Text="{Binding [Price]}" />

Binding to a nested Property in DataGrid row tooltip WPF

I canĀ“t get this working. I have a view) which contains a DataGrid populated with items of an observable collection (MyDataCollection). Every item of MyDataCollection has different properties (Name, Description,..., Logs). Logs is an observable collection itself of Log items. Every Log item has different properties (Date, Person,...).
My data grid populated with items of MyDataCollection has a tooltip per row set. Like this:
<DataGrid ItemsSource="{Binding MyDataCollection}">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="ToolTip">
<Setter.Value>
<Border>
<Grid Margin="5" MaxWidth="400">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
...
</Grid.RowDefinitions>
...
<DataGrid x:Name="LogsGrid" Grid.Row="6" ItemsSource="{Binding PlacementTarget.DataContext.Logs, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ToolTip}}">
<DataGrid.Columns>
<DataGridTextColumn Header="Date"
Binding="{Binding Date}"
/>
<DataGridTextColumn Header="Person"
Binding="{Binding Person.FullName}"
/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowStyle>
</DataGrid>
I can see the tooltip, and I can see the datagrid in the tooltip with the Headers "Date" and "Person" but the grid content is empty. Looks like the binding is not set correct. Can anyone give me a hand? Thanks
Update 1 :
MyDataColletion contains objects of my custom class "Car". Here the definition of Car:
public class Car : INotifyPropertyChanged
{
public string name;
public string description;
public Contact assignedTo;
public ObservableCollection<Log> logs = new ObservableCollection<Log>();
public string Name
{
get
{
return this.name;
}
set
{
if (this.name != value)
{
this.name = value;
NotifyPropertyChanged("Name");
}
}
}
public string Description
{
get
{
return this.description;
}
set
{
if (this.description != value)
{
this.description = value;
NotifyPropertyChanged("Description");
}
}
}
public Contact AssignedTo
{
get
{
return this.assignedTo;
}
set
{
if (this.assignedTo != value)
{
this.assignedTo = value;
NotifyPropertyChanged("AssignedTo");
}
}
}
public ObservableCollection<Log> Logs
{
get
{
return this.logs;
}
private set //TODO : Check if this is correct
{
if (this.logs != value)
{
this.logs = value;
NotifyPropertyChanged("Logs");
}
}
}
public Car()
{
// TODO: Delete this: (only here for testing)
Contact c = new Contact();
c.Name = "Test";
c.LastName = "Test";
for (int i = 0; i < 4; i++)
AddLog(DateTime.Now, c, new TimeSpan(2, 0, 0));
}
public void AddLog(DateTime date, Contact person, TimeSpan time)
{
Log newLog = new Log(date, person, time);
Logs.Add(newLog);
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
And my Log Class is:
public class Log : INotifyPropertyChanged
{
DateTime date;
Contact person;
TimeSpan time;
public DateTime Date
{
get
{
return this.date;
}
set
{
if (this.date != value)
{
this.date = value;
NotifyPropertyChanged("Date");
}
}
}
public Contact Person
{
get
{
return this.person;
}
set
{
if (this.person != value)
{
this.person = value;
NotifyPropertyChanged("Person");
}
}
}
public TimeSpan Time
{
get
{
return this.time;
}
set
{
if (this.time != value)
{
this.time = value;
NotifyPropertyChanged("Time");
}
}
}
public Log(DateTime date, Contact person, TimeSpan time)
{
this.date = date;
this.person = person;
this.time = time;
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
The only thing I can find in your code that doesn't work perfectly for me is Mode=TwoWay in the binding on LogsGrid.ItemsSource. That throws an exception for me, because it's telling the Binding that you want LogsGrid to write new values for ItemsSource back to the Binding source -- in this case, to the viewmodel's Logs property. Not only is that not what you want, but it's actually impossible since Logs has a private setter (and furthermore, DataGrid doesn't do that anyway). Hence the exception.
UpdateSourceTrigger=PropertyChanged is another one that serves no purpose, though harmlessly this time: That tells it when to write new Logs collections back to Car.Logs. But again, DataGrid can't do that. It doesn't create new values for the property. A TextBox will assign new Text values to the source property, that's what a TextBox is for. But DataGrid doesn't do that.
When I get rid of those two things, it works fine for me:
<DataGrid x:Name="LogsGrid" Grid.Row="6" ItemsSource="{Binding Logs}">
<!-- etc. -->
Instead of ItemsSource="{Binding Logs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> , use this : ItemsSource="{Binding PlacementTarget.DataContext.Logs, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ToolTip}}">.

Validating ItemsControl on Button click in WPF

I have an ItemsControl with an item template that contains two ComboBoxes. For any given item, the second ComboBox is required iff the first ComboBox has a selected value. I have set this validation up using IDataErrorInfo on the view model.
Rather than flagging ComboBox #2 as invalid the second a user selects a value in ComboBox1, I want to perform the validation when the user tries to save. It's kind of annoying to have a form "yell" at you for doing something wrong on a field you haven't even had a chance to enter yet.
Normally you could force this validation by retrieving the BindingExpression for the ComboBox and calling UpdateSource() and then determine if there is an error by calling Validation.GetHasError() passing the ComboBox. Since the ComboBoxes are generated dynamically by the ItemsControl, it is not as easy to get to. So I have 2 questions: 1. How do you ensure validation has executed for all controls when the save button is clicked. 2. How do you check whether there are validation errors when the save button is clicked. Validation.GetHasError remains false for the ItemsControl even when a ComboBox2 within it has an error. Thanks.
EDIT:
I had followed this article to implement IDataErrorInfo in order to validate the combobox properties relative to each other.
public class IntroViewModel : INotifyPropertyChanged, IDataErrorInfo
{
public Guid ClassScheduleID
{
get { return _intro.ClassScheduleID; }
set
{
_intro.ClassScheduleID = value;
OnPropertyChanged("ClassScheduleID");
//OnPropertyChanged("TrialDate"); //This will trigger validation on ComboBox2 when bound ComboBox1 changes
}
}
public DateTime TrialDate
{
get { return _intro.TrialDate; }
set
{
_intro.TrialDate = value;
OnPropertyChanged("TrialDate");
}
}
public string Error
{
get { return null; }
}
public string this[string columnName]
{
get { return ValidateProperty(columnName); }
}
private string ValidateProperty(string propertyName)
{
string error = null;
switch (propertyName)
{
case "TrialDate":
if (_intro.TrialDate == DateTime.MinValue && _intro.ClassScheduleID != Guid.Empty)
error = "Required";
break;
default:
error = null;
break;
}
return error;
}
}
I attempted to create the behavior you need based on some assumptions
sample
XAML
<StackPanel>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding AddItem}"
Content="Add Item" />
<Button Command="{Binding Save}"
Content="Save" />
</StackPanel>
<ItemsControl ItemsSource="{Binding Data}"
Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="border"
BorderThickness="1"
Padding="2"
Margin="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="value1" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ComboBox Text="{Binding Value1}"
ItemsSource="{Binding Source={StaticResource sampleData}}" />
<ComboBox Text="{Binding Value2}"
ItemsSource="{Binding Source={StaticResource sampleData}}"
Grid.Column="1" />
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsValid}"
Value="False">
<Setter TargetName="border"
Property="BorderBrush"
Value="Red" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
Main VM
public ViewModel()
{
AddItem = new SimpleCommand(i => Data.Add(new DataViewModel(new DataModel())));
Save = new SimpleCommand(i =>
{
foreach (var vm in Data)
{
vm.ValidateAndSave();
}
}
);
Data = new ObservableCollection<DataViewModel>();
}
public ObservableCollection<DataViewModel> Data { get; set; }
public ICommand AddItem { get; set; }
public ICommand Save { get; set; }
data VM and model
public class DataModel
{
public object Value1 { get; set; }
public object Value2 { get; set; }
}
public class DataViewModel : INotifyPropertyChanged
{
DataModel model;
public DataViewModel(DataModel model)
{
this.model = model;
IsValid = true;
}
object _value1;
public object Value1
{
get
{
return _value1;
}
set
{
_value1 = value;
}
}
object _value2;
public object Value2
{
get
{
return _value2;
}
set
{
_value2 = value;
}
}
public bool IsValid { get; set; }
public void ValidateAndSave()
{
IsValid = !(_value1 != null && _value2 == null);
PropertyChanged(this, new PropertyChangedEventArgs("IsValid"));
if (IsValid)
{
model.Value1 = _value1;
model.Value2 = _value2;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
so the VM will validate all the items when you click save and will save only those items which are valid. otherwise will mark the IsValid property to false which will be notified to UI
I can't tell how you've implemented the IDataErrorInfo interface in your code, but in my implementation, doing what you want is simple. For future users, you can find out about this interface on the IDataErrorInfo Interface page on MSDN. On the linked page, you will see that you need to implement the Item indexer and the Error property.
That's all you need, because if you have implemented it correctly, then you can find out if your data (implementing) item has an error by simply checking the value of the Error property:
bool hasError = string.IsNullOrEmpty(yourDataTypeInstance.Error);
if (!hasError) Save(yourDataTypeInstance);
else MessageBox.Show("Invalid data!");
UPDATE >>>
Try using this instead:
public DateTime TrialDate
{
get { return _intro.TrialDate; }
set
{
_intro.TrialDate = value;
OnPropertyChanged("TrialDate");
OnPropertyChanged("Error");
}
}
public string Error
{
get { return this["TrialDate"]; }
}
I'll leave you to work out the rest, which is essentially managing strings.
Here is how I accomplished it while waiting for answers. When a save is intiated, ValidateTrials() is called to ensure validation has fired for the comboboxes and then TrialsHaveErrors() is called to check whether there are validation errors on them. This is the brute force approach I'd like to avoid, but it does work.
//Force validation on each combobox2
private void ValidateTrials()
{
foreach (IntroViewModel introVm in icTrials.Items)
{
ContentPresenter cp = (ContentPresenter)icTrials.ItemContainerGenerator.ContainerFromItem(introVm);
if (cp == null) continue;
ComboBox cb2 = (ComboBox)cp.ContentTemplate.FindName("cb2", (FrameworkElement)cp);
//Update the source to force validation.
cb2.GetBindingExpression(ComboBox.SelectedValueProperty).UpdateSource();
}
}
//Recursively searches the Visual Tree for ComboBox elements and checks their errors state
public bool TrialsHaveError(DependencyObject ipElement)
{
if (ipElement!= null)
{
for (int x = 0; x < VisualTreeHelper.GetChildrenCount(ipElement); x++)
{
DependencyObject child = VisualTreeHelper.GetChild(ipElement, x);
if (child != null && child is ComboBox)
{
if (Validation.GetHasError(child))
return true;
}
if (TrialsHaveError(child)) return true; //We found a combobox with an error
}
}
return false;
}
Slimmed down XAML:
<ItemsControl Name="icTrials" ItemsSource="{Binding Intros}" Margin="10,6,10,0" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid Grid.Row="2">
<ComboBox Name="cb1"
SelectedValuePath="ID"
SelectedValue="{Binding Path=ClassScheduleID, Converter={StaticResource nullEmptyConverter}, ConverterParameter=System.Guid}"
ItemsSource="{Binding ClassesSource}">
<ComboBox.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox Name="cb2"
ItemsSource="{Binding AvailableStartDates}"
DisplayMemberPath="Date"
ItemStringFormat="{}{0:d}"
SelectedValue="{Binding Path=TrialDate, Converter={StaticResource nullEmptyConverter}, ConverterParameter=System.DateTime, ValidatesOnDataErrors=True}">
</ComboBox>
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
To avoid the issue of flagging the field invalid before the user has had a chance to set it, I updated the setter for cb1's bound property, ClassScheduleID to conditionally fire notification for the TrialDate property depending on how the value is changing.

Categories