如何在验证错误时禁用按钮
How to disable Button on validation error
我有一个 DataGrid
像这样:
<DataGrid CanUserSortColumns="False" CanUserAddRows="True" ItemsSource="{Binding Vertices}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn x:Name="YColumn" Width="*" Header="Latitude">
<DataGridTextColumn.Binding>
<Binding Path="Y">
<Binding.ValidationRules>
<validation:DoubleValidationRule />
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
<DataGridTextColumn x:Name="XColumn" Width="*" Header="Longitude">
<DataGridTextColumn.Binding>
<Binding Path="X">
<Binding.ValidationRules>
<validation:DoubleValidationRule />
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
我有两列具有相同的验证规则(检查单元格中的值是否为双精度值):
public class DoubleValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value != null)
{
double proposedValue;
if (!double.TryParse(value.ToString(), out proposedValue))
{
return new ValidationResult(false, "'" + value.ToString() + "' is not a whole double.");
}
}
return new ValidationResult(true, null);
}
}
这很好用,如果用户输入的值不是双精度值,单元格周围会显示红色边框。现在,如果任何单元格出现验证错误,我想禁用一个按钮。
根据有关此主题的其他一些帖子,我使用 MultiDataTriggers
:
实现了此目的
<Button>
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=XColumn}" Value="False" />
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=YColumn}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
虽然这不起作用。即使出现验证错误,该按钮也永远不会禁用。我究竟做错了什么?
编辑:这是我的模型和视图模型中的相关代码:
public class CustomVertex
{
public double X { get; set; }
public double Y { get; set; }
public CustomVertex()
{ }
}
public class CustomPolygonViewModel : ViewModelBase
{
public ObservableCollection<CustomVertex> Vertices { get; set; }
public CustomPolygonViewModel()
{
Vertices = new ObservableCollection<CustomVertex>();
}
}
我的 DataContext
设置正确,并且我验证了模型的 x 和 y 在更改值时正在更新。正确命中验证规则。
您必须让您的视图模型实现 INotifyDataErrorInfo
MSDN. Example. Example from MSDN (Silverlight)。
从 .Net 4.5 开始,这是向视图模型引入验证的推荐方法,将帮助您解决问题。
实现此接口时,您必须提供可以绑定的 HasErrors
属性。 INotifyDataErrorInfo
替换了过时的 IDataErrorInfo
.
直接绑定到 Validation.HasError
,就像您在触发器中所做的那样,将不起作用,因为 Validation.HasError
是只读附加的 属性,因此不支持绑定。为了证明这一点,我在 MSDN:
上找到了这个声明
... read-only dependency properties aren't appropriate for many of the scenarios for which dependency properties normally offer a solution (namely: data binding, directly stylable to a value, validation, animation, inheritance).
INotifyDataErrorInfo
的工作原理
当 Binding
的 ValidatesOnNotifyDataErrors
属性 设置为 true
时,绑定引擎将在绑定源上搜索 INotifyDataErrorInfo
实现订阅 ErrorsChanged
活动。
如果引发 ErrorsChanged
事件并且 HasErrors
计算结果为 true
,绑定将调用 GetErrors()
方法让实际的 属性 检索特定的错误消息并应用可自定义的验证错误模板来可视化错误。默认情况下,在经过验证的元素周围绘制红色边框。
如何实现INotifyDataErrorInfo
CustomVertex
class 实际上是 DataGrid
列的 ViewModel,因为您要绑定到它的属性。所以它必须实现INotifyDataErrorInfo
。它可能看起来像这样:
public class CustomVertex : INotifyPropertyChanged, INotifyDataErrorInfo
{
public CustomVertex()
{
this.errors = new Dictionary<string, List<string>>();
this.validationRules = new Dictionary<string, List<ValidationRule>>();
this.validationRules.Add(nameof(this.X), new List<ValidationRule>() {new DoubleValidationRule()});
this.validationRules.Add(nameof(this.Y), new List<ValidationRule>() {new DoubleValidationRule()});
}
public bool ValidateProperty(object value, [CallerMemberName] string propertyName = null)
{
lock (this.syncLock)
{
if (!this.validationRules.TryGetValue(propertyName, out List<ValidationRule> propertyValidationRules))
{
return;
}
// Clear previous errors from tested property
if (this.errors.ContainsKey(propertyName))
{
this.errors.Remove(propertyName);
OnErrorsChanged(propertyName);
}
propertyValidationRules.ForEach(
(validationRule) =>
{
ValidationResult result = validationRule.Validate(value, CultuteInfo.CurrentCulture);
if (!result.IsValid)
{
AddError(propertyName, result.ErrorContent, false);
}
}
}
}
// Adds the specified error to the errors collection if it is not
// already present, inserting it in the first position if isWarning is
// false. Raises the ErrorsChanged event if the collection changes.
public void AddError(string propertyName, string error, bool isWarning)
{
if (!this.errors.ContainsKey(propertyName))
{
this.errors[propertyName] = new List<string>();
}
if (!this.errors[propertyName].Contains(error))
{
if (isWarning)
{
this.errors[propertyName].Add(error);
}
else
{
this.errors[propertyName].Insert(0, error);
}
RaiseErrorsChanged(propertyName);
}
}
// Removes the specified error from the errors collection if it is
// present. Raises the ErrorsChanged event if the collection changes.
public void RemoveError(string propertyName, string error)
{
if (this.errors.ContainsKey(propertyName) &&
this.errors[propertyName].Contains(error))
{
this.errors[propertyName].Remove(error);
if (this.errors[propertyName].Count == 0)
{
this.errors.Remove(propertyName);
}
RaiseErrorsChanged(propertyName);
}
}
#region INotifyDataErrorInfo Members
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (String.IsNullOrEmpty(propertyName) ||
!this.errors.ContainsKey(propertyName)) return null;
return this.errors[propertyName];
}
public bool HasErrors
{
get { return errors.Count > 0; }
}
#endregion
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private double x;
public double X
{
get => x;
set
{
if (ValidateProperty(value))
{
this.x = value;
OnPropertyChanged();
}
}
}
private double y;
public double Y
{
get => this.y;
set
{
if (ValidateProperty(value))
{
this.y = value;
OnPropertyChanged();
}
}
}
private Dictionary<String, List<String>> errors;
// The ValidationRules for each property
private Dictionary<String, List<ValidationRule>> validationRules;
private object syncLock = new object();
}
观点:
<DataGrid CanUserSortColumns="False" CanUserAddRows="True" ItemsSource="{Binding Vertices}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn x:Name="YColumn"
Width="*"
Header="Latitude"
Binding="{Binding Y, ValidatesOnNotifyDataErrors=True}"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />
<DataGridTextColumn x:Name="XColumn"
Width="*"
Header="Longitude"
Binding="{Binding X, ValidatesOnNotifyDataErrors=True}"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />
</DataGrid.Columns>
</DataGrid>
以下是验证错误模板,如果您想自定义视觉表示(可选)。它通过附加的 属性 Validation.ErrorTemplate
(见上文)设置在验证元素(在本例中为 DataGridTextColumn
):
<ControlTemplate x:Key=ValidationErrorTemplate>
<StackPanel>
<!-- Placeholder for the DataGridTextColumn itself -->
<AdornedElementPlaceholder x:Name="textBox"/>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
验证失败时将被禁用的按钮(因为我不知道这个按钮在可视化树中的位置,所以我假设它共享 DataContext
的 DataGrid
列,CustomVertex
数据模型):
<Button>
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=HasErrors}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
网上有很多例子。我更新了链接以提供一些内容作为开始。
我建议将 INotifyDataErrorInfo
的实现与 INotifyPropertyChanged
一起移动到基础 class 中,并让所有视图模型继承它。这使得验证逻辑可重用并保持您的视图模型 classes 干净。
您可以更改 INotifyDataErrorInfo
的实现细节以满足要求。
备注:代码未经测试。这些片段应该有效,但旨在提供一个如何实现 INotifyDataErrorInfo
接口的示例。
我有一个 DataGrid
像这样:
<DataGrid CanUserSortColumns="False" CanUserAddRows="True" ItemsSource="{Binding Vertices}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn x:Name="YColumn" Width="*" Header="Latitude">
<DataGridTextColumn.Binding>
<Binding Path="Y">
<Binding.ValidationRules>
<validation:DoubleValidationRule />
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
<DataGridTextColumn x:Name="XColumn" Width="*" Header="Longitude">
<DataGridTextColumn.Binding>
<Binding Path="X">
<Binding.ValidationRules>
<validation:DoubleValidationRule />
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
我有两列具有相同的验证规则(检查单元格中的值是否为双精度值):
public class DoubleValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value != null)
{
double proposedValue;
if (!double.TryParse(value.ToString(), out proposedValue))
{
return new ValidationResult(false, "'" + value.ToString() + "' is not a whole double.");
}
}
return new ValidationResult(true, null);
}
}
这很好用,如果用户输入的值不是双精度值,单元格周围会显示红色边框。现在,如果任何单元格出现验证错误,我想禁用一个按钮。
根据有关此主题的其他一些帖子,我使用 MultiDataTriggers
:
<Button>
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=XColumn}" Value="False" />
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=YColumn}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
虽然这不起作用。即使出现验证错误,该按钮也永远不会禁用。我究竟做错了什么?
编辑:这是我的模型和视图模型中的相关代码:
public class CustomVertex
{
public double X { get; set; }
public double Y { get; set; }
public CustomVertex()
{ }
}
public class CustomPolygonViewModel : ViewModelBase
{
public ObservableCollection<CustomVertex> Vertices { get; set; }
public CustomPolygonViewModel()
{
Vertices = new ObservableCollection<CustomVertex>();
}
}
我的 DataContext
设置正确,并且我验证了模型的 x 和 y 在更改值时正在更新。正确命中验证规则。
您必须让您的视图模型实现 INotifyDataErrorInfo
MSDN. Example. Example from MSDN (Silverlight)。
从 .Net 4.5 开始,这是向视图模型引入验证的推荐方法,将帮助您解决问题。
实现此接口时,您必须提供可以绑定的 HasErrors
属性。 INotifyDataErrorInfo
替换了过时的 IDataErrorInfo
.
直接绑定到 Validation.HasError
,就像您在触发器中所做的那样,将不起作用,因为 Validation.HasError
是只读附加的 属性,因此不支持绑定。为了证明这一点,我在 MSDN:
... read-only dependency properties aren't appropriate for many of the scenarios for which dependency properties normally offer a solution (namely: data binding, directly stylable to a value, validation, animation, inheritance).
INotifyDataErrorInfo
的工作原理
当 Binding
的 ValidatesOnNotifyDataErrors
属性 设置为 true
时,绑定引擎将在绑定源上搜索 INotifyDataErrorInfo
实现订阅 ErrorsChanged
活动。
如果引发 ErrorsChanged
事件并且 HasErrors
计算结果为 true
,绑定将调用 GetErrors()
方法让实际的 属性 检索特定的错误消息并应用可自定义的验证错误模板来可视化错误。默认情况下,在经过验证的元素周围绘制红色边框。
如何实现INotifyDataErrorInfo
CustomVertex
class 实际上是 DataGrid
列的 ViewModel,因为您要绑定到它的属性。所以它必须实现INotifyDataErrorInfo
。它可能看起来像这样:
public class CustomVertex : INotifyPropertyChanged, INotifyDataErrorInfo
{
public CustomVertex()
{
this.errors = new Dictionary<string, List<string>>();
this.validationRules = new Dictionary<string, List<ValidationRule>>();
this.validationRules.Add(nameof(this.X), new List<ValidationRule>() {new DoubleValidationRule()});
this.validationRules.Add(nameof(this.Y), new List<ValidationRule>() {new DoubleValidationRule()});
}
public bool ValidateProperty(object value, [CallerMemberName] string propertyName = null)
{
lock (this.syncLock)
{
if (!this.validationRules.TryGetValue(propertyName, out List<ValidationRule> propertyValidationRules))
{
return;
}
// Clear previous errors from tested property
if (this.errors.ContainsKey(propertyName))
{
this.errors.Remove(propertyName);
OnErrorsChanged(propertyName);
}
propertyValidationRules.ForEach(
(validationRule) =>
{
ValidationResult result = validationRule.Validate(value, CultuteInfo.CurrentCulture);
if (!result.IsValid)
{
AddError(propertyName, result.ErrorContent, false);
}
}
}
}
// Adds the specified error to the errors collection if it is not
// already present, inserting it in the first position if isWarning is
// false. Raises the ErrorsChanged event if the collection changes.
public void AddError(string propertyName, string error, bool isWarning)
{
if (!this.errors.ContainsKey(propertyName))
{
this.errors[propertyName] = new List<string>();
}
if (!this.errors[propertyName].Contains(error))
{
if (isWarning)
{
this.errors[propertyName].Add(error);
}
else
{
this.errors[propertyName].Insert(0, error);
}
RaiseErrorsChanged(propertyName);
}
}
// Removes the specified error from the errors collection if it is
// present. Raises the ErrorsChanged event if the collection changes.
public void RemoveError(string propertyName, string error)
{
if (this.errors.ContainsKey(propertyName) &&
this.errors[propertyName].Contains(error))
{
this.errors[propertyName].Remove(error);
if (this.errors[propertyName].Count == 0)
{
this.errors.Remove(propertyName);
}
RaiseErrorsChanged(propertyName);
}
}
#region INotifyDataErrorInfo Members
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (String.IsNullOrEmpty(propertyName) ||
!this.errors.ContainsKey(propertyName)) return null;
return this.errors[propertyName];
}
public bool HasErrors
{
get { return errors.Count > 0; }
}
#endregion
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private double x;
public double X
{
get => x;
set
{
if (ValidateProperty(value))
{
this.x = value;
OnPropertyChanged();
}
}
}
private double y;
public double Y
{
get => this.y;
set
{
if (ValidateProperty(value))
{
this.y = value;
OnPropertyChanged();
}
}
}
private Dictionary<String, List<String>> errors;
// The ValidationRules for each property
private Dictionary<String, List<ValidationRule>> validationRules;
private object syncLock = new object();
}
观点:
<DataGrid CanUserSortColumns="False" CanUserAddRows="True" ItemsSource="{Binding Vertices}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn x:Name="YColumn"
Width="*"
Header="Latitude"
Binding="{Binding Y, ValidatesOnNotifyDataErrors=True}"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />
<DataGridTextColumn x:Name="XColumn"
Width="*"
Header="Longitude"
Binding="{Binding X, ValidatesOnNotifyDataErrors=True}"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />
</DataGrid.Columns>
</DataGrid>
以下是验证错误模板,如果您想自定义视觉表示(可选)。它通过附加的 属性 Validation.ErrorTemplate
(见上文)设置在验证元素(在本例中为 DataGridTextColumn
):
<ControlTemplate x:Key=ValidationErrorTemplate>
<StackPanel>
<!-- Placeholder for the DataGridTextColumn itself -->
<AdornedElementPlaceholder x:Name="textBox"/>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
验证失败时将被禁用的按钮(因为我不知道这个按钮在可视化树中的位置,所以我假设它共享 DataContext
的 DataGrid
列,CustomVertex
数据模型):
<Button>
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=HasErrors}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
网上有很多例子。我更新了链接以提供一些内容作为开始。
我建议将 INotifyDataErrorInfo
的实现与 INotifyPropertyChanged
一起移动到基础 class 中,并让所有视图模型继承它。这使得验证逻辑可重用并保持您的视图模型 classes 干净。
您可以更改 INotifyDataErrorInfo
的实现细节以满足要求。
备注:代码未经测试。这些片段应该有效,但旨在提供一个如何实现 INotifyDataErrorInfo
接口的示例。