Show validation text in separate control - c#

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]}" />

Related

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}}">.

MVVM WPF ListBox Entry not updating

I recently started to learn the MVVM Pattern and created a simple application to test a few things.
I have a simple View with:
ListBox holding ObservableCollection of Items
Delete Button
New Button
TextBox for Item Description
TextBox for Item Value
Everything works except for the fact that, if i'm updating the item description the ListBox entry isn't updating. I read some articles about this, so i think it has something to do with CollectionChanged isn't called. I tried some possible solutions to this problem, but none of them worked. So maybe there is something generally wrong with my approach.
Hopefully someone can help me with this problem.
Model/Item.cs
internal class Item : INotifyPropertyChanged
{
#region Fields
private string value;
private string description;
#endregion
#region Constructors
public Item()
{
}
public Item(string value, string description) {
this.description = description;
this.value = value;
}
#endregion
public String Value
{
get
{
return value;
}
set
{
this.value = value;
OnPropertyChanged("Value");
}
}
public String Description
{
get
{
return description;
}
set
{
description = value;
OnPropertyChanged("Description");
}
}
#region Overrides
public override string ToString()
{
return description;
}
#endregion String Override
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
ViewModel/MainViewModel.cs
...
private ObservableCollection<Item> items;
private Item selectedItem;
public ObservableCollection<Item> Items {
get
{
return items;
}
set
{
items = value;
OnPropertyChanged("Items");
}
}
public Item SelectedItem {
get
{
return selectedItem;
}
set
{
selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
...
View/MainWindow.xaml
...
<Button Content="New" Command="{Binding NewCommand}" />
<Button Content="Delete" Command="{Binding DeleteCommand}" />
<ListBox x:Name="lbxItems" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" />
<TextBox Text="{Binding SelectedItem.Description}" />
<TextBox Text="{Binding SelectedItem.Value}" />
...
with this ItemTemplate it should work
<ListBox Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Value}" Margin="0,0,10,0/>
<TextBlock Text="{Binding Path=Description}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
How you generate a PropertyChangedEvent in your model class?
try this:
internal class Range : INotifyPropertyChanged
{
private string _value;
public string Value
{
get { return _value; }
set
{
if (_value == value) return;
_value = value;
OnPropertyChanged("Value");
}
}
//Do same for the Description property
//Do not forgot implement INotifyPropertyChanged interface here
}
I hope this help you
Oh! After your update it is clearly. You does not use any datatemplate for listbox item, so WPF calls the ToString() method for show item without DT. But when you update any property, WPF does not know about this because object does not changing.
Use Datatemplate or try call OnPropertyChanged with empty string.

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.

what to do when name is empty the code gets stuck

public string Name
{
get { return name; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException("Name can not be empty");
name = value;
NotifyPropertyChanged();
}
}
what to do when name is empty the code gets stuck at name= value .....does not shows message.
what should be done?
Try like this
public string Name
{
get
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Name can not be empty");
}
return name;
}
set { name = value; }
}
If you actually need to stop the application execution in cases where name is null or empty, then by all means, throw an exception in property setter. It's not "forbidden"... but in most cases I would put the validation of properties somewhere else.
This is because if you do throw an exception then things might blow up, or you can "get stuck", as you put it. Also, if you need to reuse your model in different scenario, where properties are the same but name could be empty, what would you do then?
I don't know if your scenario involves UI or not but you can use ValidationRules to check the validity of properties and give cues to user via UI bindings.
Let's say you have a simple User model consisting of only name and age properties.
public class User : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
private int _age;
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged(); }
}
public int Age
{
get { return _age; }
set { _age = value; OnPropertyChanged(); }
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Now, you want to chekc that yes, the name is actually not null nor empty string. Then you could create simple validation rule to check for this.
public class NameLengthRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string name = value as string;
return string.IsNullOrWhiteSpace(name) ? new ValidationResult(false, "Name can't be empty") : new ValidationResult(true, null);
}
}
Now when you have these in place, and you have instance of type User in your DataContext, you can use the user input and rule on your UI side (assuming it's WPF) like so.
<Window.Resources>
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<AdornedElementPlaceholder/>
<TextBlock Foreground="Red">Invalid value!</TextBlock>
</DockPanel>
</ControlTemplate>
<Style x:Key="validationError" 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>
</Window.Resources>
<Grid>
<StackPanel>
<TextBox Validation.ErrorTemplate="{StaticResource validationTemplate}" Style="{StaticResource validationError}" Margin="150">
<TextBox.Text>
<Binding Path="User.Name" UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<local:NameLengthRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</Grid>
Which gives you the following on valid input
and the following on invalid input

How do you decouple your ViewModel properties validation from ViewModel?

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/

Categories