[英]How do you decouple your ViewModel properties validation from ViewModel?
我正在使用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);
}
}
DepartmentCode
和DepartmentFullName
綁定到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>
在保存Department之前,我需要驗證DepartmentCode
和DepartmentFullName
是否包含一些文本。
我的驗證邏輯應該放在哪里? 在ViewModel本身? 如果是這樣,我如何將我的驗證邏輯解耦,以便它也可以進行單元測試?
我發現最簡單的方法是使用a
System.Windows.Controls.ValidationRule
它只需要3個直接的步驟。
首先,您創建一個ValidationRule。 這是一個完全獨立的類,它存在於Model和ViewModel之外,並定義了如何驗證Text數據。 在這種情況下,一個簡單的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);
}
}
}
接下來,指定TextBoxes應使用新類的實例,通過指定Text綁定的ValidationRules屬性對輸入的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未通過驗證,請創建一個樣式以禁用“保存”按鈕。 我們通過綁定到我們將驗證規則綁定到的文本框的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}"/>
現在,如果您的任何一個TextBox未通過驗證,TextBox將突出顯示,並且將禁用“保存”按鈕。
DepartmentValidationRule與業務邏輯完全分離,可重用且可測試。
創建一個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;
}
}
創建一個包裝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));
}
}
}
公開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);
}
如何使用ValidationRules類,這將使您的模型與使用驗證代碼進行poppluting分離。
這對於單個控件非常有用 ,但您也可以將此邏輯委托給一些自定義驗證類, MvvmValidator框架將對您有所幫助。 此框架允許您以規則的形式編寫復雜的驗證邏輯,這些規則可以在ViewModel級別配置,並可以在提交按鈕上觸發。 它是一種很好的解耦方式,可以在不填充domian對象的情況下應用驗證。
在視圖模型中添加新方法(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);
}
}
您可以使您的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
用ValidDepartment
替換Department
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>
設置TextBox Style
和Validation.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>
希望這可以幫助
我也覺得這很煩人,因為它會將您的業務邏輯驅動到ViewModel
迫使您接受並將其留在那里或在Service Layer
或Data Model
復制它。 如果你不介意失去使用注釋等的一些優點。 這是我使用過的最常推薦的方法 - 從服務層向ValidationDictionary添加錯誤。
您還可以將這些與在服務層中按上述方式處理的業務邏輯以及在ViewModel
注釋的僅UI相關驗證混合使用。
*請注意,我從MVC的角度回答這個問題,但我認為這仍然是相關的。
我在所有項目中使用流暢的驗證,不僅可以解耦,還可以輕松地對我的驗證規則進行單元測試。 http://fluentvalidation.codeplex.com/ 。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.