WPF 绑定组验证

Posted

技术标签:

【中文标题】WPF 绑定组验证【英文标题】:WPF BindingGroup Validation 【发布时间】:2020-06-09 02:30:25 【问题描述】:

我正在使用 StackPanel 来托管多个附加了验证规则的 TextBox。 我还有一个 StackPanel.BindingGroup 验证,请参见下面的代码:

我有一个名为 ValidateAll 的 BindingGroup 验证规则,我想从中在我的状态栏上的 TextBlock 中显示错误消息。 我只想显示 ValidateAll 消息,因为 TextBox 验证消息显示在 TextBoxes 下方。

我想为我的 TextBlock 设置一个样式,在该样式中我只能显示来自我的 BindingGroup 的验证错误消息(ValidateAll 规则)。

我知道我可以通过处理 ItemError 事件在代码中执行此操作,在该事件中,我可以通过 ValidationError.RuleInError 属性获取与错误消息关联的规则(见下文)。

我希望能够在 xaml 中完成此操作,可能通过为我的 StatusBar TextBlock 设置样式/触发器/设置器组合。 任何帮助将不胜感激。

代码:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Globalization;

namespace WpfGroupValidationDemo2

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    
        public MainWindow()
        
            InitializeComponent();
            this.DataContext = new ViewModel();
        

        private void PreviewTextBoxKeyUp(object sender, System.Windows.Input.KeyEventArgs e)
        
            this.TextBoxStack.BindingGroup.UpdateSources();
        

        // This event occurs when a ValidationRule in the BindingGroup or in a Binding fails.
        private void ItemError(object sender, ValidationErrorEventArgs e)
        

            if ((e.Action == ValidationErrorEventAction.Added) &&
                (e.Error.RuleInError.ToString() == "WpfGroupValidationDemo2.ValidateAll"))
            
                StatusTextBlock.Text = e.Error.ErrorContent.ToString();
                
            else
                StatusTextBlock.Text = String.Empty;
        
    

    public class ViewModel : INotifyPropertyChanged
    

        public event PropertyChangedEventHandler PropertyChanged;   

        public ViewModel()
          
            this.name = "Allan";
            this.age = 30;
        

        #region Properties

        private string name;
        public string Name
        
            get  return this.name; 
            set
            
                if (value != name)
                
                    this.name = value;
                    this.OnPropertyChanged(nameof(Name));
                
            
        
        private int age;
        public int Age
        
            get  return this.age; 
            set
            
                if (value != this.age)
                
                    this.age = value;
                    this.OnPropertyChanged(nameof(Age));
                
            
        

  #endregion Properties

        private void OnPropertyChanged([CallerMemberName] String propertyName = "")
        
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            
                handler(this, new PropertyChangedEventArgs(propertyName));
            
        
    

  #region Validation Rules
    public class ValidateAgeRule : ValidationRule
    
        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        
            if (!int.TryParse(value.ToString(), out int i))
                return new ValidationResult(false, "Please enter a valid integer value.");

            if (i < 30 || i > 70)
                return new ValidationResult(false, "Age must be between 30 and 70");

            return new ValidationResult(true, null);

        
    

    public class ValidateNameRule : ValidationRule
    
        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        
            string name = (string)value;
            if (name != "Allan" && name != "Jim")
                return new ValidationResult(false, "Please enter the names: Allan or Jim");

            return new ValidationResult(true, null);
        
    


    public class ValidateAll : ValidationRule
    

        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        
            if (value == null)
                return ValidationResult.ValidResult;

            BindingGroup bg = value as BindingGroup;

            ViewModel viewModel = bg.Items[0] as ViewModel;

            object ageValue;
            object nameValue;

            // Get the proposed values for age and name 
            bool ageResult = bg.TryGetValue(viewModel, "Age", out ageValue);
            bool nameResult = bg.TryGetValue(viewModel, "Name", out nameValue);

            if (!ageResult || !nameResult)
                return new ValidationResult(false, "Properties not found");

            int age = (int)ageValue;
            string name = (string)nameValue;

            if ((age == 30 ) && (name == "Jim"))
                return new ValidationResult(false, "Jim cannot be Thirty!");

            return ValidationResult.ValidResult;
        
    

  #endregion Validation Rules

XAML:

Window x:Class="WpfGroupValidationDemo2.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfGroupValidationDemo2"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">

    <Window.Resources>
        <ControlTemplate x:Key="validationTemplate" >
            <StackPanel>
                <!--Placeholder for the TextBox itself-->
                <AdornedElementPlaceholder/>
                <TextBlock Text="Binding [0].ErrorContent" Foreground="Red" Background="DynamicResource x:Static SystemColors.ControlLightLightBrushKey"/>
            </StackPanel>
        </ControlTemplate>

        <!-- Add a red border on validation error to a textbox control -->
        <Style x:Key="TextBoxBorderStyle" TargetType="TextBox">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="x:Type TextBox">
                        <Border x:Name="bg" BorderBrush="#FFABADB3" BorderThickness="1">
                            <ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="TemplateBinding SnapsToDevicePixels"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="Validation.HasError" Value="True" >
                                <Trigger.Setters>
                                    <Setter Property="BorderBrush" TargetName="bg"  Value="Red"/>
                                    <Setter Property="BorderThickness" TargetName="bg" Value="1"/>
                                    <Setter Property="SnapsToDevicePixels" TargetName="bg" Value="True"/>
                                </Trigger.Setters>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

    </Window.Resources>

    <Grid>
        <StackPanel HorizontalAlignment="Left" Height="204" Margin="168,125,0,0" VerticalAlignment="Top" Width="409" RenderTransformOrigin="0.5,0.5" Orientation="Horizontal">
            <StackPanel Width="184" HorizontalAlignment="Right">
                <Label Content="Name:" HorizontalAlignment="Right" Margin="0,3"/>
                <Label Content="Age:" HorizontalAlignment="Right"/>
            </StackPanel>
        <StackPanel  Name="TextBoxStack" Width="200" Height="202" Validation.ErrorTemplate="x:Null" Validation.Error="ItemError">
            <StackPanel.BindingGroup>
                <BindingGroup Name="ValidateAllFields" NotifyOnValidationError="True">
                    <BindingGroup.ValidationRules>
                        <local:ValidateAll ValidationStep="ConvertedProposedValue"/>
                    </BindingGroup.ValidationRules>
                </BindingGroup>
            </StackPanel.BindingGroup>
            <TextBox x:Name="NameTextBox" Style="StaticResource TextBoxBorderStyle" TextWrapping="Wrap" Height="26" VerticalContentAlignment="Center" 
                         Margin="0,3,130,3" Validation.ErrorTemplate="StaticResource validationTemplate" PreviewKeyUp="PreviewTextBoxKeyUp">
                <TextBox.Text>
                    <Binding Path="Name" UpdateSourceTrigger="PropertyChanged">
                        <Binding.ValidationRules>
                            <local:ValidateNameRule ValidationStep="RawProposedValue" ValidatesOnTargetUpdated="True"/>
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>
            <TextBox x:Name="AgeTextBox" Style="StaticResource TextBoxBorderStyle" Height="26" TextWrapping="Wrap" VerticalContentAlignment="Center" 
                         Margin="0,0,130,3" Validation.ErrorTemplate="StaticResource validationTemplate" PreviewKeyUp="PreviewTextBoxKeyUp">
                <TextBox.Text>
                    <Binding Path="Age" UpdateSourceTrigger="PropertyChanged">
                        <Binding.ValidationRules>
                            <local:ValidateAgeRule ValidationStep="RawProposedValue" ValidatesOnTargetUpdated="True"/>
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>
        </StackPanel>
    </StackPanel>
    <Label Content="BindingGroup Demo" HorizontalAlignment="Left" Margin="204,78,0,0" VerticalAlignment="Top" Width="305"/>
        <Label Content="Only Visible when All the textboxes pass validation!" HorizontalAlignment="Left" Margin="417,332,0,0" VerticalAlignment="Top" Width="286" >
            <Label.Style>
                <Style TargetType="x:Type Label">
                    <Setter Property="Visibility" Value="Hidden" />
                    <Style.Triggers>
                        <!-- Require the controls to be valid in order to be visible -->
                        <MultiDataTrigger>
                            <MultiDataTrigger.Conditions>
                                <Condition Binding="Binding ElementName=NameTextBox, Path=(Validation.HasError)" Value="false" />
                                <Condition Binding="Binding ElementName=AgeTextBox, Path=(Validation.HasError)" Value="false" />
                                <Condition Binding="Binding ElementName=TextBoxStack, Path=(Validation.HasError)" Value="false" />
                        </MultiDataTrigger.Conditions>
                            <Setter Property="Visibility" Value="Visible" />
                        </MultiDataTrigger>
                    </Style.Triggers>
                </Style>
            </Label.Style>
        </Label>
        <StatusBar Margin="4,0,0,1" VerticalAlignment="Bottom" VerticalContentAlignment="Bottom" Padding="0,3" >
            <StatusBarItem>
                <TextBlock Name="StatusTextBlock" Foreground="Red" />
            </StatusBarItem>
        </StatusBar>
    </Grid>

【问题讨论】:

这篇文章可能对你有帮助how-to-implement-binding-validation 已添加代码以帮助说明我遇到的问题。 我想在 xaml 而不是代码中实现所有 ItemError 逻辑,这样我就可以将 TextBlock 用于所有消息,而不仅仅是错误消息。我可以将 ValidateAll 消息的前景变为红色并切换回任何其他消息。 【参考方案1】:

好的,在***上其他问题和回复的帮助下,我想通了:

XAML 更改:我为我的 StatusBar:TextBlock 添加了一个样式,它在具有 BindingGroup 的 StackPanel 绑定上实现了一个 DataTrigger。 Trigger 获取当前的 Validation RuleInError,它是一个 ValidationRule,并通过 IValueConverter 将其转换为字符串。

文本块样式:

<Style x:Key="TextBlockStyle" TargetType="TextBlock">
    <Setter Property="Foreground" Value="#FF000000"/>
    <Style.Triggers>
        <DataTrigger Binding="Binding ElementName=TextBoxStack, Path=(Validation.Errors)[0].RuleInError, 
                Converter=StaticResource RuleConverterClass" Value="True" >
            <Setter Property="Foreground" Value="Red" />
        </DataTrigger>
    </Style.Triggers>
</Style>

对于此解决方案,StackPanel 不会引发 Validation.Error,也不会使用 handler:ItemError。 StatusBar TextBlock 已更新为使用新样式:

<StatusBar Margin="4,0,0,1" VerticalAlignment="Bottom" VerticalContentAlignment="Bottom" Padding="0,3" >
    <StatusBarItem>
        <TextBlock Name="StatusTextBlock" Style="StaticResource TextBlockStyle" />
    </StatusBarItem>
</StatusBar>

代码更改: 更新了Button Click Event调用BindingGroup.UpdateSources()函数完成校验,(ValidateAll校验规则):

private void ButtonClick(object sender, RoutedEventArgs e)

    //this.TextBoxStack.BindingGroup.UpdateSources();

    if (!this.TextBoxStack.BindingGroup.UpdateSources())
        StatusTextBlock.Text = (string)this.TextBoxStack.BindingGroup.ValidationErrors[0].ErrorContent;
    else
        StatusTextBlock.Text = "Calculation Successful";

添加转换类将验证规则对象转换为文本字符串:

[ValueConversion(typeof(ValidationRule), typeof(Boolean))]
public class ValidationRuleConverter : IValueConverter

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    
        bool returnValue = false;
        ValidationRule rule = (ValidationRule)value;
        string name = rule.ToString();

        if (name == "WpfGroupValidationDemo4.ValidateAll")
            returnValue = true;

        return returnValue;
    
    
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    
        return value;
    

这基本上就是我想要做的。这将为我提供窗口级别验证,并允许我在状态栏上显示验证消息。使用这种技术,我可以显示所有窗口级别的消息并更改文本颜色以反映消息的严重性,同时保持 MVVM 的精神。

【讨论】:

以上是关于WPF 绑定组验证的主要内容,如果未能解决你的问题,请参考以下文章

WPF 数据验证中的绑定失败

WPF - 从组标题样式中绑定到当前项目

WPF 数据绑定和验证规则最佳实践

将单选按钮组绑定到 WPF 中的属性

WPF DataGrid 验证/绑定模式错误

2021-09-15 WPF上位机 16-属性绑定(数据验证)