如何在 XAML 数据验证失败时让视图模型知道

How can I let my view model know when XAML data validation has failed

提问人:spainchaud 提问时间:2/8/2023 更新时间:2/10/2023 访问量:58

问:

我有一个表格,用户可以在其中设置数值过程的参数。每个参数对象都有一个默认值。

    public double DefaultValue
    {
        get => _defaultValue;
        set
        {
            _defaultValue = value;
            OnPropertyChanged("DefaultValue");
        }
    }

尽管该属性是双精度值,但它可能表示布尔值或整数。对于大多数参数,不需要验证,但我有两个参数,最小值和最大值,它们是有限的。我不能有最小值>最大值或最大值<最小值。我已经在 XAML 中实现了验证,如果数据无效,它会直观地警告用户。Min参数的数据模板如下。

    <DataTemplate x:Key="MinParameterDataTemplateThin">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="120"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="{Binding DisplayName, StringFormat='{}{0}:'}" Grid.Column="0" Margin="10,5,5,10" VerticalAlignment="Top" TextWrapping="Wrap"
                       Visibility="{Binding Visibility}" ToolTipService.ShowDuration="20000">
                <TextBlock.ToolTip>
                    <ToolTip DataContext="{Binding Path=PlacementTarget.DataContext, RelativeSource={x:Static RelativeSource.Self}}">
                        <TextBlock Text="{Binding Description}"/>
                    </ToolTip>
                </TextBlock.ToolTip>                                
            </TextBlock>

            <Grid Grid.Column="1">
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <StackPanel Grid.Row="0" Orientation="Horizontal">
                    <TextBox Name ="MinTextBox" Margin="5" Width="50" VerticalAlignment="Top"
                             Visibility="{Binding Visibility}" IsEnabled="{Binding IsEnabled}">
                        <TextBox.Resources>
                            <validations:BindingProxy x:Key="proxy" Data="{Binding}"/>
                        </TextBox.Resources>
                        <TextBox.Text>
                            <Binding Path="DefaultValue" StringFormat="N2" Mode="TwoWay"
                                     UpdateSourceTrigger="LostFocus"
                                     ValidatesOnExceptions="True"
                                     NotifyOnValidationError="True"
                                     ValidatesOnNotifyDataErrors="True">
                                <Binding.ValidationRules>
                                    <validations:MaximumValueValidation ValidatesOnTargetUpdated="True">
                                        <validations:MaximumValueValidation.MaxValueWrapper>
                                            <validations:MaxValueWrapper MaxValue="{Binding Data.MaxValue, Source={StaticResource proxy}}"/>
                                        </validations:MaximumValueValidation.MaxValueWrapper>
                                    </validations:MaximumValueValidation>
                                </Binding.ValidationRules>
                            </Binding>
                        </TextBox.Text>
                    </TextBox>
                    <TextBlock Text="{Binding UnitSymbol}" Margin="5" VerticalAlignment="Top" Visibility="{Binding Visibility}"/>
                </StackPanel>
                <Label Name="ValidationLabel" Content="{Binding ElementName=MinTextBox, Path=(Validation.Errors)[0].ErrorContent}" Foreground="Red" Grid.Row="1" VerticalAlignment="Top"/>

            </Grid>
        </Grid>
    </DataTemplate>

Max 参数有一个类似的模板。除了视觉警告之外,我还需要阻止用户保存数据。我希望在参数对象中有一个布尔值 IsValid 属性,以便在用户尝试保存时进行测试。如何从 XAML 绑定到此 IsValid 属性?

C# WPF 验证 XAML

评论

0赞 mm8 2/8/2023
在视图模型中实现接口,不要依赖视图中的验证规则来验证数据。验证规则对 MVVM 不是很友好。INotifyDataErrorInfo
0赞 Andy 2/10/2023
Inotifydataerrorinfo 非常适合验证数据是否实际到达视图模型,当数据传输失败时就不是那么好了。视图模型中包含无效数据也可能有点麻烦。我在我的答案中添加了更多代码和链接

答:

0赞 Andy 2/8/2023 #1

当您看到带有红色边框的错误出现时,会引发一个路由错误事件,该事件将通过可视化树冒泡。

当您修复错误时,您会收到相同的事件,该事件会引发告诉您错误已修复。

因此,您可以将发生的错误相加并减去已修复的错误。如果你的得分大于零,你就有一些东西需要修复。

这是 Validation.ErrorEvent

https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.validation.error?view=windowsdesktop-7.0

然后,您可以将结果传递给 viewmodel,或调用命令来传递事件结果,然后 viewmodel 将执行该添加。

下面是一些标记和代码。

在所有可能出错的控件的父级中:

    <i:Interaction.Triggers>
        <UIlib:RoutedEventTrigger RoutedEvent="{x:Static Validation.ErrorEvent}">
            <e2c:EventToCommand
                 Command="{Binding ConversionErrorCommand, Mode=OneWay}"
                 EventArgsConverter="{StaticResource BindingErrorEventArgsConverter}"
                 PassEventArgsToCommand="True" />
        </UIlib:RoutedEventTrigger>

不确定这是否仍然适合复制粘贴,因为它现在有点旧了。

    public RelayCommand<PropertyError> ConversionErrorCommand
    {
        get
        {
            return conversionErrorCommand
                ?? (conversionErrorCommand = new RelayCommand<PropertyError>
                    (PropertyError =>
                    {
                        if (PropertyError.Added)
                        {
                            AddError(PropertyError.PropertyName, PropertyError.Error, ErrorSource.Conversion);
                        }
                        FlattenErrorList();
                    }));
        }
    }

转炉

public class BindingErrorEventArgsConverter : IEventArgsConverter
{
    public object Convert(object value, object parameter)
    {
        ValidationErrorEventArgs e = (ValidationErrorEventArgs)value;
        PropertyError err = new PropertyError();
        err.PropertyName = ((System.Windows.Data.BindingExpression)(e.Error.BindingInError)).ResolvedSourcePropertyName;
        err.Error = e.Error.ErrorContent.ToString();
        // Validation.ErrorEvent fires both when an error is added AND removed
        if (e.Action == ValidationErrorEventAction.Added)
        {
            err.Added = true;
        }
        else
        {
            err.Added = false;
        }
        return err;
    }
}

路由事件触发器

// This is necessary in order to grab the bubbling routed source changed and conversion errors
public class RoutedEventTrigger : EventTriggerBase<DependencyObject>
{
    RoutedEvent routedEvent;
    public RoutedEvent RoutedEvent
    {
        get
        {
            return routedEvent;
        }
        set
        {
            routedEvent = value;
        }
    }

    public RoutedEventTrigger()
    {
    }
    protected override void OnAttached()
    {
        Behavior behavior = base.AssociatedObject as Behavior;
        FrameworkElement associatedElement = base.AssociatedObject as FrameworkElement;
        if (behavior != null)
        {
            associatedElement = ((IAttachedObject)behavior).AssociatedObject as FrameworkElement;
        }
        if (associatedElement == null)
        {
            throw new ArgumentException("This only works with framework elements");
        }
        if (RoutedEvent != null)
        {
            associatedElement.AddHandler(RoutedEvent, new RoutedEventHandler(this.OnRoutedEvent));
        }
    }
    void OnRoutedEvent(object sender, RoutedEventArgs args)
    {
        base.OnEvent(args);
        args.Handled = true;
    }
    protected override string GetEventName()
    {
        return RoutedEvent.Name;
    }
}

命令的事件来自 mvvm light。现已弃用,但代码仍然有效。

你可能会喜欢

MVVM 将 EventArgs 作为命令参数传递

我认为这解释了棱镜方法:

https://weblogs.asp.net/alexeyzakharov/silverlight-commands-hacks-passing-eventargs-as-commandparameter-to-delegatecommand-triggered-by-eventtrigger

但我认为 mvvmlight 是开源的,这是 eventtocommand 的代码:

// ****************************************************************************
// <copyright file="EventToCommand.cs" company="GalaSoft Laurent Bugnion">
// Copyright © GalaSoft Laurent Bugnion 2009-2016
// </copyright>
// ****************************************************************************
// <author>Laurent Bugnion</author>
// <email>[email protected]</email>
// <date>3.11.2009</date>
// <project>GalaSoft.MvvmLight.Extras</project>
// <web>http://www.mvvmlight.net</web>
// <license>
// See license.txt in this solution or http://www.galasoft.ch/license_MIT.txt
// </license>
// ****************************************************************************

using Microsoft.Xaml.Behaviors;
using System;
using System.Windows;
using System.Windows.Input;

////using GalaSoft.Utilities.Attributes;

namespace GalaSoft.MvvmLight.CommandWpf
{
    /// <summary>
    /// This <see cref="T:System.Windows.Interactivity.TriggerAction`1" /> can be
    /// used to bind any event on any FrameworkElement to an <see cref="ICommand" />.
    /// Typically, this element is used in XAML to connect the attached element
    /// to a command located in a ViewModel. This trigger can only be attached
    /// to a FrameworkElement or a class deriving from FrameworkElement.
    /// <para>To access the EventArgs of the fired event, use a RelayCommand&lt;EventArgs&gt;
    /// and leave the CommandParameter and CommandParameterValue empty!</para>
    /// </summary>
    ////[ClassInfo(typeof(EventToCommand),
    ////  VersionString = "5.2.8",
    ////  DateString = "201504252130",
    ////  Description = "A Trigger used to bind any event to an ICommand.",
    ////  UrlContacts = "http://www.galasoft.ch/contact_en.html",
    ////  Email = "[email protected]")]
    public class EventToCommand : TriggerAction<DependencyObject>
    {
        /// <summary>
        /// Identifies the <see cref="CommandParameter" /> dependency property
        /// </summary>
        public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(
            "CommandParameter",
            typeof(object),
            typeof(EventToCommand),
            new PropertyMetadata(
                null,
                (s, e) =>
                {
                    var sender = s as EventToCommand;
                    if (sender == null)
                    {
                        return;
                    }

                    if (sender.AssociatedObject == null)
                    {
                        return;
                    }

                    sender.EnableDisableElement();
                }));

        /// <summary>
        /// Identifies the <see cref="Command" /> dependency property
        /// </summary>
        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
            "Command",
            typeof(ICommand),
            typeof(EventToCommand),
            new PropertyMetadata(
                null,
                (s, e) => OnCommandChanged(s as EventToCommand, e)));

        /// <summary>
        /// Identifies the <see cref="MustToggleIsEnabled" /> dependency property
        /// </summary>
        public static readonly DependencyProperty MustToggleIsEnabledProperty = DependencyProperty.Register(
            "MustToggleIsEnabled",
            typeof(bool),
            typeof(EventToCommand),
            new PropertyMetadata(
                false,
                (s, e) =>
                {
                    var sender = s as EventToCommand;
                    if (sender == null)
                    {
                        return;
                    }

                    if (sender.AssociatedObject == null)
                    {
                        return;
                    }

                    sender.EnableDisableElement();
                }));

        private object _commandParameterValue;

        private bool? _mustToggleValue;

        /// <summary>
        /// Gets or sets the ICommand that this trigger is bound to. This
        /// is a DependencyProperty.
        /// </summary>
        public ICommand Command
        {
            get
            {
                return (ICommand) GetValue(CommandProperty);
            }

            set
            {
                SetValue(CommandProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets an object that will be passed to the <see cref="Command" />
        /// attached to this trigger. This is a DependencyProperty.
        /// </summary>
        public object CommandParameter
        {
            get
            {
                return GetValue(CommandParameterProperty);
            }

            set
            {
                SetValue(CommandParameterProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets an object that will be passed to the <see cref="Command" />
        /// attached to this trigger. This property is here for compatibility
        /// with the Silverlight version. This is NOT a DependencyProperty.
        /// For databinding, use the <see cref="CommandParameter" /> property.
        /// </summary>
        public object CommandParameterValue
        {
            get
            {
                return _commandParameterValue ?? CommandParameter;
            }

            set
            {
                _commandParameterValue = value;
                EnableDisableElement();
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the attached element must be
        /// disabled when the <see cref="Command" /> property's CanExecuteChanged
        /// event fires. If this property is true, and the command's CanExecute 
        /// method returns false, the element will be disabled. If this property
        /// is false, the element will not be disabled when the command's
        /// CanExecute method changes. This is a DependencyProperty.
        /// </summary>
        public bool MustToggleIsEnabled
        {
            get
            {
                return (bool) GetValue(MustToggleIsEnabledProperty);
            }

            set
            {
                SetValue(MustToggleIsEnabledProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the attached element must be
        /// disabled when the <see cref="Command" /> property's CanExecuteChanged
        /// event fires. If this property is true, and the command's CanExecute 
        /// method returns false, the element will be disabled. This property is here for
        /// compatibility with the Silverlight version. This is NOT a DependencyProperty.
        /// For databinding, use the <see cref="MustToggleIsEnabled" /> property.
        /// </summary>
        public bool MustToggleIsEnabledValue
        {
            get
            {
                return _mustToggleValue == null
                           ? MustToggleIsEnabled
                           : _mustToggleValue.Value;
            }

            set
            {
                _mustToggleValue = value;
                EnableDisableElement();
            }
        }

        /// <summary>
        /// Called when this trigger is attached to a FrameworkElement.
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
            EnableDisableElement();
        }

#if SILVERLIGHT
        private Control GetAssociatedObject()
        {
            return AssociatedObject as Control;
        }
#else
        /// <summary>
        /// This method is here for compatibility
        /// with the Silverlight version.
        /// </summary>
        /// <returns>The FrameworkElement to which this trigger
        /// is attached.</returns>
        private FrameworkElement GetAssociatedObject()
        {
            return AssociatedObject as FrameworkElement;
        }
#endif

        /// <summary>
        /// This method is here for compatibility
        /// with the Silverlight 3 version.
        /// </summary>
        /// <returns>The command that must be executed when
        /// this trigger is invoked.</returns>
        private ICommand GetCommand()
        {
            return Command;
        }

        /// <summary>
        /// Specifies whether the EventArgs of the event that triggered this
        /// action should be passed to the bound RelayCommand. If this is true,
        /// the command should accept arguments of the corresponding
        /// type (for example RelayCommand&lt;MouseButtonEventArgs&gt;).
        /// </summary>
        public bool PassEventArgsToCommand
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a converter used to convert the EventArgs when using
        /// <see cref="PassEventArgsToCommand"/>. If PassEventArgsToCommand is false,
        /// this property is never used.
        /// </summary>
        public IEventArgsConverter EventArgsConverter
        {
            get;
            set;
        }

        /// <summary>
        /// The <see cref="EventArgsConverterParameter" /> dependency property's name.
        /// </summary>
        public const string EventArgsConverterParameterPropertyName = "EventArgsConverterParameter";

        /// <summary>
        /// Gets or sets a parameters for the converter used to convert the EventArgs when using
        /// <see cref="PassEventArgsToCommand"/>. If PassEventArgsToCommand is false,
        /// this property is never used. This is a dependency property.
        /// </summary>
        public object EventArgsConverterParameter
        {
            get
            {
                return GetValue(EventArgsConverterParameterProperty);
            }
            set
            {
                SetValue(EventArgsConverterParameterProperty, value);
            }
        }

        /// <summary>
        /// Identifies the <see cref="EventArgsConverterParameter" /> dependency property.
        /// </summary>
        public static readonly DependencyProperty EventArgsConverterParameterProperty = DependencyProperty.Register(
            EventArgsConverterParameterPropertyName,
            typeof(object),
            typeof(EventToCommand),
            new PropertyMetadata(null));

        /// <summary>
        /// The <see cref="AlwaysInvokeCommand" /> dependency property's name.
        /// </summary>
        public const string AlwaysInvokeCommandPropertyName = "AlwaysInvokeCommand";

        /// <summary>
        /// Gets or sets a value indicating if the command should be invoked even
        /// if the attached control is disabled. This is a dependency property.
        /// </summary>
        public bool AlwaysInvokeCommand
        {
            get
            {
                return (bool)GetValue(AlwaysInvokeCommandProperty);
            }
            set
            {
                SetValue(AlwaysInvokeCommandProperty, value);
            }
        }

        /// <summary>
        /// Identifies the <see cref="AlwaysInvokeCommand" /> dependency property.
        /// </summary>
        public static readonly DependencyProperty AlwaysInvokeCommandProperty = DependencyProperty.Register(
            AlwaysInvokeCommandPropertyName,
            typeof(bool),
            typeof(EventToCommand),
            new PropertyMetadata(false));


        /// <summary>
        /// Provides a simple way to invoke this trigger programatically
        /// without any EventArgs.
        /// </summary>
        public void Invoke()
        {
            Invoke(null);
        }

        /// <summary>
        /// Executes the trigger.
        /// <para>To access the EventArgs of the fired event, use a RelayCommand&lt;EventArgs&gt;
        /// and leave the CommandParameter and CommandParameterValue empty!</para>
        /// </summary>
        /// <param name="parameter">The EventArgs of the fired event.</param>
        protected override void Invoke(object parameter)
        {
            if (AssociatedElementIsDisabled() 
                && !AlwaysInvokeCommand)
            {
                return;
            }

            var command = GetCommand();
            var commandParameter = CommandParameterValue;

            if (commandParameter == null
                && PassEventArgsToCommand)
            {
                commandParameter = EventArgsConverter == null
                    ? parameter
                    : EventArgsConverter.Convert(parameter, EventArgsConverterParameter);
            }

            if (command != null
                && command.CanExecute(commandParameter))
            {
                command.Execute(commandParameter);
            }
        }

        private static void OnCommandChanged(
            EventToCommand element,
            DependencyPropertyChangedEventArgs e)
        {
            if (element == null)
            {
                return;
            }

            if (e.OldValue != null)
            {
                ((ICommand) e.OldValue).CanExecuteChanged -= element.OnCommandCanExecuteChanged;
            }

            var command = (ICommand) e.NewValue;

            if (command != null)
            {
                command.CanExecuteChanged += element.OnCommandCanExecuteChanged;
            }

            element.EnableDisableElement();
        }

        private bool AssociatedElementIsDisabled()
        {
            var element = GetAssociatedObject();

            return AssociatedObject == null
                || (element != null
                   && !element.IsEnabled);
        }

        private void EnableDisableElement()
        {
            var element = GetAssociatedObject();

            if (element == null)
            {
                return;
            }

            var command = GetCommand();

            if (MustToggleIsEnabledValue
                && command != null)
            {
                element.IsEnabled = command.CanExecute(CommandParameterValue);
            }
        }

        private void OnCommandCanExecuteChanged(object sender, EventArgs e)
        {
            EnableDisableElement();
        }
    }
}

我认为您可能需要ieventargsconverter

namespace GalaSoft.MvvmLight.CommandWpf
{
    /// <summary>
    /// The definition of the converter used to convert an EventArgs
    /// in the <see cref="EventToCommand"/> class, if the
    /// <see cref="EventToCommand.PassEventArgsToCommand"/> property is true.
    /// Set an instance of this class to the <see cref="EventToCommand.EventArgsConverter"/>
    /// property of the EventToCommand instance.
    /// </summary>
    ////[ClassInfo(typeof(EventToCommand))]
    public interface IEventArgsConverter
    {
        /// <summary>
        /// The method used to convert the EventArgs instance.
        /// </summary>
        /// <param name="value">An instance of EventArgs passed by the
        /// event that the EventToCommand instance is handling.</param>
        /// <param name="parameter">An optional parameter used for the conversion. Use
        /// the <see cref="EventToCommand.EventArgsConverterParameter"/> property
        /// to set this value. This may be null.</param>
        /// <returns>The converted value.</returns>
        object Convert(object value, object parameter);
    }
}

评论

0赞 spainchaud 2/10/2023
我已经实现了大部分代码,当发生验证错误时,我可以将对象传递给我的视图模型,但我无法弄清楚如何使用转换器并传递 PropertyError 对象。我没有访问 EventToCommand,我有 InvokeCommandAction。我似乎无法正确设置我的 CommandParameter。
0赞 spainchaud 2/10/2023
我还应该注意,我可以访问 Prism 和 Telerik 库。
0赞 spainchaud 2/11/2023
谢谢。由于功能冻结即将到来,我采取了一种稍微不同的方法,以便向测试人员提供一些东西。我将验证错误字符串作为参数传递给 ConversionErrorCommand。虽然不像传递事件那样灵活或有趣,但它将一直工作到我们应用程序的下一个版本。我将为此添加书签,因为我将来会想使用 EventToCommand。