I use WPF data binding with entities that implement IDataErrorInfo interface. In general my code looks like this:
Business entity:
p
I think @Stanislav Kniazev approach is the correct one. Your comment about not adding logic to the business object is also valid. To have a clean separation of concern, how about keeping the Person in the business layer (or the data model layer), and introduce a new class PersonVm with view logic. For the VM layer I like the containment pattern more than inheritence, and at this layer I also implement INotifyPropertyChanged, which is also a property of the VM and not the data model.
public class PersonVm : IDataErrorInfo, INotifyPropertyChanged
{
private Person _person;
public PersonVm( ) {
// default constructor
_person = new Person( );
_dirty = false;
}
public PersonVm( Person p ) {
// User this constructor when you get a Person from database or network
_person = p;
_dirty = false;
}
void fire( string prop ) {
PropertyChanged( this, new PropertyChangedEventArgs( prop ) );
}
public string name {
get { return _person.name; }
set { _person.name = value; fire( "name" ); dirty = true; }
}
...
string IDataErrorInfo.this[string columnName] {
get {
if( dirty ) return _person[columnName];
}
}
}
The idea is to put the logic of each layer at the appropriate class. At the data model layer, you do validation that only concern the pure data. The the View Model layer, you add logic that concerns the View Model (as well as notificaton and other View Model logic).
I'm just a junior developer who doesn't have many knowledge but I fixed it this way.
In my validationresult class I've made a constructor with no parameters to return a valid validationresult.
public class NotEmptyValidation : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (string.IsNullOrEmpty(value as string))
{
return new ValidationResult(false,"Veld kan niet leeg zijn");
}
return new ValidationResult(true,null);
}
public NotEmptyValidation() : base()
{
Validate();
}
public ValidationResult Validate()
{
return new ValidationResult(true,null);
}
}
My xaml code looks like this
<!--TEXTBOXES-->
<TextBox Grid.Column="1" Grid.ColumnSpan="2" Margin="5">
<TextBox.Text>
<Binding Path="SelectedEntity.Code" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<val:NotEmptyValidation />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="1" Margin="5">
<TextBox.Text>
<Binding Path="SelectedEntity.Name" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<val:NotEmptyValidation />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
When my form loads, the validation doesnt fire when the window loads, but if I clear a textbox, it does fire.
There is a downside to this, if I load an invalid Entity wich has an emty name or code, the validation doesnt fire at loading the window, it does however when u fill in the textbox and clear it. But this doesn't really happen since I validate all my fields when I create the entity..
Its not a perfect solution but it works for me.
You can change your person class to fire validation error only if Name property was ever changed:
public class Person : IDataErrorInfo {
private bool nameChanged = false;
private string name;
public string Name {
get { return name; }
set {
name = value;
nameChanged = true;
}
}
//... skipped some code
string IDataErrorInfo.this[string columnName] {
get {
if(nameChanged && columnName == "Name" && string.IsNullOrEmpty(Name))
return "Name is not entered";
return string.Empty;
}
}
}
Damn this took a while to think out but as always,...attached behaviours to the rescue.
What you're looking at in essence is dirty state tracking. There are many ways to do this using your ViewModel but since you didn't want to change your entities the best way is using behaviours.
First off remove the ValidatesOnDataErrors from your Xaml binding. Create a behaviour for the control you're working on ( as shown below for TextBox
) and in the TextChanged
event (or whatever event you want) reset the binding to one that does validate on data errors. Simple really.
This way, your entities don't have to change, your Xaml is kept reasonably clean and you get your behaviour.
Here's the behaviour code-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace IDataErrorInfoSample
{
public static class DirtyStateBehaviours
{
public static string GetDirtyBindingProperty(DependencyObject obj)
{
return (string)obj.GetValue(DirtyBindingPropertyProperty);
}
public static void SetDirtyBindingProperty(DependencyObject obj, string value)
{
obj.SetValue(DirtyBindingPropertyProperty, value);
}
// Using a DependencyProperty as the backing store for DirtyBindingProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DirtyBindingPropertyProperty =
DependencyProperty.RegisterAttached("DirtyBindingProperty", typeof(string), typeof(DirtyStateBehaviours),
new PropertyMetadata(new PropertyChangedCallback(Callback)));
public static void Callback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
var textbox = obj as TextBox;
textbox.TextChanged += (o, s) =>
{
Binding b = new Binding(GetDirtyBindingProperty(textbox));
b.ValidatesOnDataErrors = true;
textbox.SetBinding(TextBox.TextProperty, b);
};
}
}
}
And the Xaml is pretty straight forward too.
<Window x:Class="IDataErrorInfoSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:IDataErrorInfoSample"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="MainWindow"
Height="350"
Width="525">
<Window.DataContext>
<local:Person />
</Window.DataContext>
<StackPanel Margin="20">
<TextBox Height="20"
Margin="0,0,0,10"
local:DirtyStateBehaviours.DirtyBindingProperty="Name"
Text="{Binding Path=Name}">
</TextBox>
<Button Content="Go" />
</StackPanel>
HTH, Stimul8d.
Maybe it's an option for you, to shift your validation to the View: Instead of implementing IDataErrorInfo, you can enable NotifyOnValidationError in your binding and add a ValidationRule that does the check. For ValidationRules there is a standard way to control, if the rule should be applied when the object changes (not the property value directly)
This will already provide visual feedback to the user (ErrorTemplate will be applied). If you need more, e.g. disable some buttons etc, you can wire up the Validation.Error-Event of your view to your ViewModel or BusinessEntity, s.th. you can identify there, if any error is present.
There is another solution which i found but i don't like it a lot. You have to clear validation on page load.
What i mean is you have to do this :
Validation.ClearInvalid(...)
for instance if you have a textbox you dont want to be validated should call
Validation.ClearInvalid(txtSomething.GetBindingExpression(TextBox.TextProperty))
or something like that.
You should do this for every control you want to be cleared of validation.
I didn't like the solution but that was the best i found. I hoped wpf had something "out of the box" that worked but didn't find it.