简体   繁体   English

如何从ViewModel中分离ViewModel属性验证?

[英]How do you decouple your ViewModel properties validation from ViewModel?

I am using MVVMLight. 我正在使用MVVMLight。 This is my Department model/POCO class. 这是我的Department模型/ POCO课程。 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 : 这是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. DepartmentCodeDepartmentFullName绑定到UI,如下所示。

 <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. 在保存Department之前,我需要验证DepartmentCodeDepartmentFullName是否包含一些文本。

Where should my validation logic reside ? 我的验证逻辑应该放在哪里? In ViewModel itself ? 在ViewModel本身? 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 我发现最简单的方法是使用a

System.Windows.Controls.ValidationRule

It only takes 3 straight-forward steps. 它只需要3个直接的步骤。

First you create a ValidationRule. 首先,您创建一个ValidationRule。 This is a completely separate class that exists outside both your Model and ViewModel and defines how the Text data should be validated. 这是一个完全独立的类,它存在于Model和ViewModel之外,并定义了如何验证Text数据。 In this case a simple String.IsNullOrWhiteSpace check. 在这种情况下,一个简单的String.IsNullOrWhiteSpace检查。

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. 接下来,指定TextBoxes应使用新类的实例,通过指定Text绑定的ValidationRules属性对输入的Text执行验证。 You get the added bonus of the TextBox border turning red if the Validation fails. 如果验证失败,您将获得TextBox边框变红的额外奖励。

    <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. 最后,如果TextBox未通过验证,请创建一个样式以禁用“保存”按钮。 We do this by binding to the Validation.HasError property of the Textbox we bound our Validation rule to. 我们通过绑定到我们将验证规则绑定到的文本框的Validation.HasError属性来完成此操作。 We'll name this style DisableOnValidationError just to make things obvious. 我们将此样式命名为DisableOnValidationError,以使事情变得明显。

    <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 最后我们在Save按钮上设置DisableOnValidationError样式

    <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. 现在,如果您的任何一个TextBox未通过验证,TextBox将突出显示,并且将禁用“保存”按钮。

The DepartmentValidationRule is completely separate from your business logic and is reusable and testable. DepartmentValidationRule与业务逻辑完全分离,可重用且可测试。

Create a DepartmentValidator class, which will be easily unit tested. 创建一个DepartmentValidator类,它将很容易进行单元测试。 Also, this class will allow you to eliminate duplication of validation in the server-side and UI scenarios. 此外,此类还允许您消除服务器端和UI方案中的重复验证。

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. 创建一个包装Department模型并实现IDataErrorInfo的DepartmentViewModel,这样您就可以使用标准验证模板进行更精细的控制并显示验证错误。

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. 公开DepartmentViewModel而不是Department Model,并将PropertyChanged事件连接到CreateDepartmentCommand,以便在部门验证失败时自动禁用Save按钮,以便显示验证错误。 Expose a ValidationErrors property. 公开ValidationErrors属性。

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. 如何使用ValidationRules类,这将使您的模型与使用验证代码进行poppluting分离。

This will work great for individual controls but you can also delegate this logic to some custom validation classes , MvvmValidator framework will help you. 这对于单个控件非常有用 ,但您也可以将此逻辑委托给一些自定义验证类, MvvmValidator框架将对您有所帮助。 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. 此框架允许您以规则的形式编写复杂的验证逻辑,这些规则可以在ViewModel级别配置,并可以在提交按钮上触发。 its a nice decouple way of applying validations without populating your domian objects. 它是一种很好的解耦方式,可以在不填充domian对象的情况下应用验证。

Add new method in your view model (Is Valid) and Modify the CanExecte method, you can easily test this by testing the CanExecute method: 在视图模型中添加新方法(Is Valid)和修改CanExecte方法,您可以通过测试CanExecute方法轻松测试:

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. 您可以使您的Model类实现IDataErrorInfo接口。

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 ViewModelValidDepartment替换Department

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 在您的View集中, ValidatesOnDataErrors=True到绑定控件

<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 : 设置TextBox StyleValidation.ErrorTemplate以确定验证在UI中的显示方式,例如,通过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 您可以了解更多有关验证在WPF 这里 ,并在这里

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 . 我也觉得这很烦人,因为它会将您的业务逻辑驱动到ViewModel迫使您接受并将其留在那里或在Service LayerData 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. 如果你不介意失去使用注释等的一些优点。 是我使用过的最常推荐的方法 - 从服务层向ValidationDictionary添加错误。

You can also mix these, with business logic handled as above in your service layer, and UI-only relevant validations annotated in your ViewModel . 您还可以将这些与在服务层中按上述方式处理的业务逻辑以及在ViewModel注释的仅UI相关验证混合使用。

*Note That I am answering this from a MVC perspective, but I think it is all still relevant. *请注意,我从MVC的角度回答这个问题,但我认为这仍然是相关的。

I use fluent validation in all my projects, not only to decouple, but also easily unit test my validation rules. 我在所有项目中使用流畅的验证,不仅可以解耦,还可以轻松地对我的验证规则进行单元测试。 http://fluentvalidation.codeplex.com/ . http://fluentvalidation.codeplex.com/

It also has a nuget package http://www.nuget.org/packages/FluentValidation/ 它还有一个nuget包http://www.nuget.org/packages/FluentValidation/

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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