首页 > 解决方案 > WPF 绑定组验证

问题描述

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

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

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

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

我希望能够在 xaml 中完成此操作,可能通过为我的 StatusBar TextBlock 设置 Style/Trigger/Setter 组合。任何帮助将非常感激。

代码:

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>

标签: wpfxamldata-binding

解决方案


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

XAML 更改:我为我的 StatusBar:TextBlock 添加了一个样式,它在具有 BindingGroup 的 StackPanel 绑定上实现了一个 DataTrigger。触发器采用当前的 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,不使用处理程序: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 的精神。


推荐阅读