0

I have a form where users set parameters for a numerical process. Each parameter object has a default value.

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

Although the property is a double, it might represent a Boolean, or an integer. For most parameters validation is not required, but I have two parameters, Min and Max, which are limited. I must not have Min > Max or Max < Min. I have implemented validation in XAML, which visually warns the user if the data is not valid. The data template for the Min parameter is as follows.

    <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>

There is a similar template for the Max parameter. In addition to the visual warning, I need to prevent the user from saving the data. I would like to have a Boolean IsValid property in the parameter object to test when the user tries to save. How would I bind from the XAML to this IsValid property?

spainchaud
  • 365
  • 3
  • 12
  • Implement the `INotifyDataErrorInfo` interface in your view model and don't rely on validation rules in the view to validate your data. Validation rules are not very MVVM friendly. – mm8 Feb 08 '23 at 12:28
  • Inotifydataerrorinfo is great for validating data actually gets to the viewmodel, not so great when data fails to transfer. It can also be a bit of a nuisance to have invalid data in your viewmodel. I added some more code and links to my answer – Andy Feb 10 '23 at 10:40

1 Answers1

0

When you see an error appear with a red border, there is a routed error event raised which will bubble up through the visual tree.

When you fix an error then you get the same event raised tells you that the error is fixed.

You can therefore add up errors occur and subtract errors fixed. If you have more than zero you have something needs fixing.

This is the Validation.ErrorEvent

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

You can then pass the result to the viewmodel or call a command to pass the event result and your viewmodel does that addition.

Here's some markup and code.

In a parent of all the controls which may error:

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

Not sure this will still be copy paste friendly since it's a bit old now.

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

Converter

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;
    }
}

Routedeventtrigger

// 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;
    }
}

Event to command is from mvvm light. Which is now deprecated but the code still works.

You might like

MVVM Passing EventArgs As Command Parameter

I think this explains a prism approach:

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

But I think mvvmlight is open source, here's the code for eventtocommand:

// ****************************************************************************
// <copyright file="EventToCommand.cs" company="GalaSoft Laurent Bugnion">
// Copyright © GalaSoft Laurent Bugnion 2009-2016
// </copyright>
// ****************************************************************************
// <author>Laurent Bugnion</author>
// <email>laurent@galasoft.ch</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 = "laurent@galasoft.ch")]
    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();
        }
    }
}

I think you may need 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);
    }
}
Andy
  • 11,864
  • 2
  • 17
  • 20
  • I have implemented much of this code, and when the validation error occurs, I can pass an object to my viewmodel, but I cannot figure out how to use the converter and pass the PropertyError object. I do not have access to EventToCommand, I have InvokeCommandAction. I cannot seem to set up my CommandParameter properly. – spainchaud Feb 09 '23 at 21:13
  • I should also note that I have access to Prism and Telerik libraries. – spainchaud Feb 09 '23 at 21:44
  • Thanks. Because a feature freeze is coming, I took a slightly different approach, in order to get something to the tester. I passed the validation error string as a parameter to the ConversionErrorCommand. While not as flexible or as interesting as passing the event, it will work until the next version of our application. I will bookmark this, because I will want to use the EventToCommand in the future. – spainchaud Feb 10 '23 at 23:15