MVVM 中的绑定验证.HasError 属性

本文关键字:HasError 属性 验证 绑定 MVVM | 更新日期: 2023-09-27 17:56:31

我目前正在实施一个ValidationRule来检查文本框中是否存在一些无效字符。我很高兴设置我实现的继承ValidationRule的类在我的文本框上将其设置为红色,当找到此类字符时,我还想使用 Validation.HasError 属性或 Validation.Errors 属性弹出一个消息框,告诉用户页面中的各种文本框中存在错误。

有没有办法将ViewModel中的属性绑定到Validation.HasError和/或 Validation.Errors 属性,以便我可以在 ViewModel 中访问它们?

这是我对文本框的错误样式:

<Style x:Key="ErrorValidationTextBox" TargetType="{x:Type pres:OneTextBox}">
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <DockPanel LastChildFill="True">
                    <TextBlock DockPanel.Dock="Right"
                    Foreground="Red"
                    FontSize="12pt"
                    Text="{Binding ElementName=MyAdorner, 
                           Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
                    </TextBlock>
                    <AdornedElementPlaceholder x:Name="MyAdorner"/>
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

下面介绍如何在 XAML 中声明我的文本框(OneTextBox 封装常规 WPF 文本框):

<pres:OneTextBox Watermark="Name..." Margin="85,12,0,0" Style="{StaticResource ErrorValidationTextBox}"
                 AcceptsReturn="False" MaxLines="1" Height="22" VerticalAlignment="Top"
                 HorizontalAlignment="Left" Width="300" >
    <pres:OneTextBox.Text>
        <Binding Path="InterfaceSpecification.Name" UpdateSourceTrigger="PropertyChanged">                    
            <Binding.ValidationRules>                       
                <interfaceSpecsModule:NoInvalidCharsRule/>                        
            </Binding.ValidationRules>                    
        </Binding>               
    </pres:OneTextBox.Text>        
</pres:OneTextBox>

MVVM 中的绑定验证.HasError 属性

Validation.HasError只读属性,因此Binding不适用于此属性。这可以从ILSpy中看到:

public virtual bool HasError
{
    get
    {
        return this._validationError != null;
    }
}

作为替代方案,您应该看到一个很好的article,它以使用附加依赖项属性的形式提供了解决方案,在那里您将看到示例的详细说明。

下面是本文的完整示例,我只是在C#下翻译了它,原始语言VB.NET

XAML

<Window x:Class="HasErrorTestValidation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:HasErrorTestValidation"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <local:TestData />
    </Window.DataContext>
    <StackPanel>
        <TextBox x:Name="TestTextBox" 
                 local:ProtocolSettingsLayout.MVVMHasError="{Binding Path=HasError}">
            <TextBox.Text>
                <Binding Path="TestText" UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <local:OnlyNumbersValidationRule ValidatesOnTargetUpdated="True"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    
        <TextBlock>
            <TextBlock.Text>
                <Binding Path="HasError" StringFormat="HasError is {0}"/>
            </TextBlock.Text>
        </TextBlock>
    
        <TextBlock>
            <TextBlock.Text>
                <Binding Path="(Validation.HasError)" ElementName="TestTextBox" StringFormat="Validation.HasError is {0}"/>
            </TextBlock.Text>
        </TextBlock>        
    </StackPanel>
</Window>

Code-behind

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}
#region Model
public class TestData : INotifyPropertyChanged
{
    private bool _hasError = false;
    public bool HasError
    {
        get
        {
            return _hasError;
        }
        set
        {
            _hasError = value;
            NotifyPropertyChanged("HasError");
        }
    }
    private string _testText = "0";
    public string TestText
    {
        get
        {
            return _testText;
        }
        set
        {
            _testText = value;
            NotifyPropertyChanged("TestText");
        }
    }
    #region PropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string sProp)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(sProp));
        }
    }
    #endregion
}
#endregion
#region ValidationRule
public class OnlyNumbersValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        var result = new ValidationResult(true, null);
        string NumberPattern = @"^[0-9-]+$";
        Regex rgx = new Regex(NumberPattern);
        if (rgx.IsMatch(value.ToString()) == false)
        {
            result = new ValidationResult(false, "Must be only numbers");
        }
        return result;
    }
}
#endregion
public class ProtocolSettingsLayout
{       
    public static readonly DependencyProperty MVVMHasErrorProperty= DependencyProperty.RegisterAttached("MVVMHasError", 
                                                                    typeof(bool),
                                                                    typeof(ProtocolSettingsLayout),
                                                                    new FrameworkPropertyMetadata(false, 
                                                                                                  FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                                                  null,
                                                                                                  CoerceMVVMHasError));
    public static bool GetMVVMHasError(DependencyObject d)
    {
        return (bool)d.GetValue(MVVMHasErrorProperty);
    }
    public static void SetMVVMHasError(DependencyObject d, bool value)
    {
        d.SetValue(MVVMHasErrorProperty, value);
    }
    private static object CoerceMVVMHasError(DependencyObject d,Object baseValue)
    {
        bool ret = (bool)baseValue;
        if (BindingOperations.IsDataBound(d,MVVMHasErrorProperty))
        {
            if (GetHasErrorDescriptor(d)==null)
            {
                DependencyPropertyDescriptor desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
                desc.AddValueChanged(d,OnHasErrorChanged);
                SetHasErrorDescriptor(d, desc);
                ret = System.Windows.Controls.Validation.GetHasError(d);
            }
        }
        else
        {
            if (GetHasErrorDescriptor(d)!=null)
            {
                DependencyPropertyDescriptor desc= GetHasErrorDescriptor(d);
                desc.RemoveValueChanged(d, OnHasErrorChanged);
                SetHasErrorDescriptor(d, null);
            }
        }
        return ret;
    }
    private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor", 
                                                                            typeof(DependencyPropertyDescriptor),
                                                                            typeof(ProtocolSettingsLayout));
    private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
    {
        var ret = d.GetValue(HasErrorDescriptorProperty);
        return ret as DependencyPropertyDescriptor;
    }
    private static void OnHasErrorChanged(object sender, EventArgs e)
    {
        DependencyObject d = sender as DependencyObject;
        if (d != null)
        {
            d.SetValue(MVVMHasErrorProperty, d.GetValue(Validation.HasErrorProperty));
        }
    }
   private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
   {
        var ret = d.GetValue(HasErrorDescriptorProperty);
        d.SetValue(HasErrorDescriptorProperty, value);
    }
}

作为使用ValidationRule的替代方案,在MVVM风格中,您可以尝试实现IDataErrorInfo接口。有关更多信息,请参阅以下内容:

使用 WPF 强制实施复杂的业务数据规则

所有完美的工作集都NotifyOnValidationError="True"绑定;(或者也可能带有绑定组)

然后使用

<Button IsEnabled="{Binding ElementName=tbPeriod, Path=(Validation.HasError)}"

使用一个文本框的示例:

<val:RangeRule可以更改为MS样本年龄范围规则等

<TextBox MaxLength="5" x:Name="tbPeriod" HorizontalAlignment="Left" VerticalAlignment="Top" Width="162" Margin="10,10,0,0" Style="{StaticResource TextBoxInError}">
            <TextBox.Text>
                <Binding Path="ReportPeriod" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
                    <Binding.ValidationRules>
                        <val:RangeRule Min="70" Max="5000" />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>

为了回应Anatoliy对非工作项目示例的请求:

通用.xaml

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestAttachedPropertyValidationError">

<Style TargetType="{x:Type local:TextBoxCustomControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:TextBoxCustomControl}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="10"/>
                            <ColumnDefinition Width="50"/>
                        </Grid.ColumnDefinitions>
                        <Grid.Resources>
                            <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
                        </Grid.Resources>
                        <Label 
                            Grid.Row ="0" 
                            Grid.Column="0" 
                            Content="Enter a numeric value:" />
                        <TextBox 
                            Grid.Row ="0" 
                            Grid.Column="2" 
                            local:HasErrorUtility.HasError="{Binding NumericPropHasError, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
                            Text="{Binding NumericProp, Mode=TwoWay, UpdateSourceTrigger=LostFocus, RelativeSource={RelativeSource TemplatedParent}}" />
                        <Label 
                            Grid.Row ="1" 
                            Grid.Column="0" 
                            Content="Value entered:" />
                        <Label 
                            Grid.Row ="1" 
                            Grid.Column="2" 
                            Content="{TemplateBinding NumericProp}" />
                        <Label 
                            Grid.Row ="2" 
                            Grid.Column="0" 
                            Grid.ColumnSpan="3" 
                            Visibility="{TemplateBinding NumericPropHasError, Converter={StaticResource BooleanToVisibilityConverter}}"
                            Foreground="Red" 
                            Content="Not a numeric value" />
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

文本框自定义控件.cs

using System.Windows;
using System.Windows.Controls;
namespace TestAttachedPropertyValidationError
{
    public class TextBoxCustomControl : Control
    {
        static TextBoxCustomControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBoxCustomControl), new FrameworkPropertyMetadata(typeof(TextBoxCustomControl)));
        }
        public static readonly DependencyProperty NumericPropProperty =
            DependencyProperty.Register("NumericProp", typeof (int), typeof (TextBoxCustomControl), new PropertyMetadata(default(int)));
        public int NumericProp
        {
            get { return (int) GetValue(NumericPropProperty); }
            set { SetValue(NumericPropProperty, value); }
        }
        public static readonly DependencyProperty NumericPropHasErrorProperty =
            DependencyProperty.Register("NumericPropHasError", typeof (bool), typeof (TextBoxCustomControl), new PropertyMetadata(default(bool)));
        public bool NumericPropHasError
        {
            get { return (bool) GetValue(NumericPropHasErrorProperty); }
            set { SetValue(NumericPropHasErrorProperty, value); }
        }
    }
}

HasErrorUtility.cs

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace TestAttachedPropertyValidationError
{
    class HasErrorUtility
    {
        public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached("HasError",
                                                                        typeof(bool),
                                                                        typeof(HasErrorUtility),
                                                                        new FrameworkPropertyMetadata(false,
                                                                                                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                                                      null,
                                                                                                      CoerceHasError));
        public static bool GetHasError(DependencyObject d)
        {
            return (bool)d.GetValue(HasErrorProperty);
        }
        public static void SetHasError(DependencyObject d, bool value)
        {
            d.SetValue(HasErrorProperty, value);
        }
        private static object CoerceHasError(DependencyObject d, Object baseValue)
        {
            var ret = (bool)baseValue;
            if (BindingOperations.IsDataBound(d, HasErrorProperty))
            {
                if (GetHasErrorDescriptor(d) == null)
                {
                    var desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
                    desc.AddValueChanged(d, OnHasErrorChanged);
                    SetHasErrorDescriptor(d, desc);
                    ret = Validation.GetHasError(d);
                }
            }
            else
            {
                if (GetHasErrorDescriptor(d) != null)
                {
                    var desc = GetHasErrorDescriptor(d);
                    desc.RemoveValueChanged(d, OnHasErrorChanged);
                    SetHasErrorDescriptor(d, null);
                }
            }
            return ret;
        }
        private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor",
                                                                                typeof(DependencyPropertyDescriptor),
                                                                                typeof(HasErrorUtility));
        private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
        {
            var ret = d.GetValue(HasErrorDescriptorProperty);
            return ret as DependencyPropertyDescriptor;
        }
        private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
        {
            d.SetValue(HasErrorDescriptorProperty, value);
        }
        private static void OnHasErrorChanged(object sender, EventArgs e)
        {
            var d = sender as DependencyObject;
            if (d != null)
            {
                d.SetValue(HasErrorProperty, d.GetValue(Validation.HasErrorProperty));
            }
        }
    }
}

视图模型.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace TestAttachedPropertyValidationError
{
    public class ViewModel :INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private int _vmNumericProp;
        private bool _vmNumericPropHasError;
        public int VmNumericProp
        {
            get { return _vmNumericProp; }
            set
            {
                _vmNumericProp = value;
                OnPropertyChanged();
            }
        }
        public bool VmNumericPropHasError
        {
            get { return _vmNumericPropHasError; }
            set
            {
                _vmNumericPropHasError = value;
                OnPropertyChanged();
            }
        }
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

MainWindow.xaml

<Window x:Class="TestAttachedPropertyValidationError.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestAttachedPropertyValidationError"
    Title="MainWindow" Height="350" Width="525">
<StackPanel Margin="10">
    <StackPanel.Resources>
        <local:ViewModel x:Key="VM1"/>
        <local:ViewModel x:Key="VM2"/>
    </StackPanel.Resources>
    <Label Content="Custom Control...}"></Label>
    <local:TextBoxCustomControl 
        Margin="10" 
        DataContext="{StaticResource VM1}"
        NumericProp="{Binding VmNumericProp}"
        NumericPropHasError="{Binding VmNumericPropHasError}"/>
    <Label Content="Regular XAML...}" Margin="0,20,0,0"/>
    <Grid 
        Margin="10"
        DataContext="{StaticResource VM2}"
        >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="10"/>
            <ColumnDefinition Width="50"/>
        </Grid.ColumnDefinitions>
        <Grid.Resources>
            <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
        </Grid.Resources>
        <Label 
                            Grid.Row ="0" 
                            Grid.Column="0" 
                            Content="Enter a numeric value:" />
        <TextBox 
                            Grid.Row ="0" 
                            Grid.Column="2" 
                            local:HasErrorUtility.HasError="{Binding VmNumericPropHasError, Mode=TwoWay}"
                            Text="{Binding VmNumericProp, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
        <Label 
                            Grid.Row ="1" 
                            Grid.Column="0" 
                            Content="Value entered:" />
        <Label 
                            Grid.Row ="1" 
                            Grid.Column="2" 
                            Content="{Binding VmNumericProp}" />
        <Label 
                            Grid.Row ="2" 
                            Grid.Column="0" 
                            Grid.ColumnSpan="3" 
                            Visibility="{Binding VmNumericPropHasError, Converter={StaticResource BooleanToVisibilityConverter}}"
                            Foreground="Red" 
                            Content="Not a numeric value" />
    </Grid>
</StackPanel>