简体   繁体   中英

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.

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);
}

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.

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

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/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM