Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 ?

like image 529
NoobDeveloper Avatar asked Sep 23 '13 12:09

NoobDeveloper


4 Answers

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.

like image 160
Vinny Avatar answered Nov 15 '22 13:11

Vinny


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.

like image 34
TalentTuner Avatar answered Nov 15 '22 12:11

TalentTuner


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);
}
like image 22
gͫrͣeͬeͨn Avatar answered Nov 15 '22 12:11

gͫrͣeͬeͨn


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.

like image 45
Matthew Avatar answered Nov 15 '22 12:11

Matthew