如何在验证错误时禁用按钮

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 的工作原理

BindingValidatesOnNotifyDataErrors 属性 设置为 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>

验证失败时将被禁用的按钮(因为我不知道这个按钮在可视化树中的位置,所以我假设它共享 DataContextDataGrid列,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 接口的示例。