11

我正在使用 MVVMLight。这是我的Department模型/POCO 课程。我不想以任何方式污染它。

 public partial class Department
    {
        public int DepartmentId { get; set; }
        public string DepartmentCode { get; set; }
        public string DepartmentFullName { get; set; }
    }

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

和绑定到 UI DepartmentCodeDepartmentFullName 如下所示。

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

在保存部门之前,我需要验证两者DepartmentCodeDepartmentFullName在其中包含一些文本。

我的验证逻辑应该放在哪里?在 ViewModel 本身?如果是这样,我如何解耦我的验证逻辑,以便它也是可单元测试的?

4

7 回答 7

8

我发现完成此任务的最简单方法是使用

System.Windows.Controls.ValidationRule

它只需要 3 个简单的步骤。

首先,您创建一个 ValidationRule。这是一个完全独立的类,存在于您的模型和视图模型之外,并定义了如何验证文本数据。在这种情况下,一个简单的 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);
        }
    }
}

接下来,通过指定 Text 绑定的 ValidationRules 属性,指定您的 TextBoxes 应使用新类的实例对输入的 Text 执行验证。如果验证失败,您将获得 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>

最后,如果任一 TextBox 验证失败,则创建一个 Style 以禁用 Save 按钮。我们通过绑定到我们将验证规则绑定到的文本框的 Validation.HasError 属性来做到这一点。我们将这种风格命名为 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>

最后我们在 Save 按钮上设置 DisableOnValidationError 样式

    <Button Grid.Row="2" Content="Save" Width="50" Command="{Binding CreateDepartmentCommand}"
            Style="{StaticResource DisableOnValidationError}"/>

现在,如果您的任何一个 TextBoxes 验证失败,则 TextBox 会突出显示,并且 Save 按钮将被禁用。

DepartmentValidationRule 与您的业务逻辑完全分离,并且是可重用和可测试的。

于 2013-09-30T20:55:06.060 回答
2

使用ValidationRules类怎么样,这将使您的模型与使用验证代码弹出它分离。

这对于单个控件非常有用,但您也可以将此逻辑委托给一些自定义验证类,MvvmValidator 框架将为您提供帮助。该框架允许您以规则的形式编写复杂的验证逻辑,这些规则可以在 ViewModel 级别进行配置,并且可以在提交按钮上触发。它是一种很好的解耦方式,可以在不填充您的 domian 对象的情况下应用验证。

于 2013-09-23T12:42:27.160 回答
2

创建一个 DepartmentValidator 类,该类将很容易进行单元测试。此外,此类将允许您消除服务器端和 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;
    }
}

创建一个包装您的部门模型并实现 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));
        }
    }
}

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

在保存部门之前,您可能需要再次验证。

private void CreateDepartment()
{
    if(Department.Error!=null)
    {
       ValidationErrors = Department.Error;
       RaisePropertyChanged("validationErrors");
       return;
    }

    bool success = departmentService.SaveDepartment(_department);
}
于 2013-09-26T11:02:14.453 回答
1

我也觉得这很烦人,因为它驱使您的业务逻辑ViewModel迫使您接受它并将其留在那里或将其复制到Service Layeror中Data Model。如果您不介意失去使用注释等的一些优势。是我使用过并且最推荐的一种方法 -从服务层向ValidationDictionary添加错误。

您还可以将这些与在服务层中处理的业务逻辑以及在您的ViewModel.

*请注意,我是从 MVC 的角度回答这个问题,但我认为这仍然是相关的。

于 2013-09-25T22:28:27.477 回答
0

我在所有项目中都使用流利的验证,不仅可以解耦,还可以轻松地对我的验证规则进行单元测试。http://fluentvalidation.codeplex.com/

它还有一个 nuget 包http://www.nuget.org/packages/FluentValidation/

于 2013-10-02T13:04:20.940 回答
0

在视图模型中添加新方法(有效)并修改 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);
}
}
于 2013-09-23T12:16:16.460 回答
0

你可以让你的Model类实现IDataErrorInfo接口。

如果你不想污染你的模型,你可以创建一个继承自它的新类,并在那里进行验证

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
}

在你ViewModel替换DepartmentValidDepartment

private ValidDepartment _department = new ValidDepartment ();
public ValidDepartment Department
{
    get
    {
        return _department;
    }
    set
    {
        if (_department == value)
        {
            return;
        }
        _department = value;
        RaisePropertyChanged("Department");
    }
}

在您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>

设置 TextBoxStyleValidation.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>

您可以在此处此处了解有关 WPF 验证的更多信息

希望这可以帮助

于 2013-09-23T12:16:33.857 回答