C# MVVM中的Binding Validation.HasError属性

C# MVVM中的Binding Validation.HasError属性,c#,wpf,validation,mvvm,binding,C#,Wpf,Validation,Mvvm,Binding,我目前正在实现一个ValidationRule,以检查文本框中是否有无效字符。我很高兴在我的文本框中设置我所实现的继承ValidationRule的类,在找到此类字符时将其设置为红色,但是我还想使用Validation.HasError属性或Validation.Errors属性弹出一个消息框,告诉用户页面中的各个文本框中存在错误 是否有方法将myViewModel中的属性绑定到Validation.HasError和/或Validation.Errors属性,以便我可以在我的ViewModel

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

是否有方法将my
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>

Validation.HasError是只读属性,因此
绑定
将无法使用此属性。这可以从以下几个方面看出:

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

下面是本文的完整示例,我刚刚在
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>
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样式中,您可以尝试实现接口。有关更多信息,请参阅:


针对安纳托利提出的非工作项目示例请求:

Generic.xaml HasErrorUtility.cs ViewModel.cs MainWindow.xaml


绑定时所有完美工作集
NotifyOnValidationError=“True”
; (或者也可以使用绑定组)

然后使用

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

只是好奇:您是否有意在视图和视图模型之间传播验证逻辑?@AndreasH。是的,与我的验证规则一样,唯一发生的事情是文本框的边框变为红色。它不会阻止用户输入更多字符(即使当存在无效字符时,Name属性停止更新),也不会阻止用户使用该名称创建InterfaceSpecification。我可以在表单的“提交”按钮上绑定Validation.HasError,但我使用的是自定义框架,无法访问该按钮(不幸的是,它在表单控件中是私有的)。谢谢您的回答!至于IDataErrorInfo,我已经对它进行了一些研究,但据我所知,最终结果与ValidationRule相同:它只在视图中向用户显示存在错误,它不会阻止用户继续程序流,直到错误得到解决。而且,正如我在我的问题中所做的评论中提到的,我使用的是一个自定义框架,它不允许我访问表单的“提交”按钮,因此当不幸出现错误时,我自己无法禁用该按钮(这就是我试图找到解决方法的原因).@choub890:如果新值无效,您可以阻止在setter中设置viewmodel的值,但会引发INotifyPropertyChanged事件,因此文本框会使用未更改的旧值进行更新。这在普通xaml中可以正常工作,但在自定义控件中,当我尝试使用模板绑定绑定附加属性时,强制emvvmhasserror(…)中的BindingOperations.IsDataBound(d,MVVMHasErrorProperty)调用返回false,因此永远不会安装回调。关于如何在自定义控件中工作,你有什么想法吗?@Brian Stewart:你能给出一个非工作项目的具体例子吗?时间会怎样,我会看的。@Anatoliy Nikolaev:我添加了一个示例作为这个问题的答案,说明您的技术使用标准XAML,而当XAML放在自定义控件中时不起作用。谢谢我不太理解您的示例。
Validation.HasError
在使用
ValidationRule
时主要需要,在您的示例中则不是。此外,在我的计算机(Windows 7)中,这两个示例在强制()函数中都返回false。此示例的含义是,
绑定
在默认情况下不在
验证中工作。HasError
,因为此属性不是依赖属性,此示例弥补了此缺点。此外,在自定义控件中,只需将bool值分配给依赖项属性->在这种情况下,通常不会在HasErrorUtility中调用强制函数。
<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>
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); }
        }
    }
}
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));
            }
        }

    }
}
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));
        }
    }
}
<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>
<Button IsEnabled="{Binding ElementName=tbPeriod, Path=(Validation.HasError)}"
<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>