0

I have an (part of) XAML file like this

                                <TextBox.Text>
                                    <Binding Path="MyProperty"
                                             UpdateSourceTrigger="PropertyChanged"
                                             TargetNullValue="">
                                        <Binding.ValidationRules>
                                            <validation:IntRangeRule Min="-999"
                                                                     Max="999" />
                                        </Binding.ValidationRules>
                                    </Binding>
                                </TextBox.Text>

with IntRangeRule class like this

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (value != null)
        {
            var stringToConvert = value as string;

            if (!string.IsNullOrEmpty(stringToConvert))
            {
                Int32 number;

                bool result = Int32.TryParse(stringToConvert, NumberStyles.Integer, cultureInfo, out number);
                if (!result)
                {
                    var localizer = ServiceLocator.Current.GetInstance<IResourceLocalizer>();
                    return new ValidationResult(false, string.Format(localizer["IntValidationError"], stringToConvert, Min, Max));
                }

                if ((number < Min) || (number > Max))
                {
                    var localizer = ServiceLocator.Current.GetInstance<IResourceLocalizer>();
                    return new ValidationResult(false, string.Format(localizer["IntRangeValidationError"], Min, Max));
                }
            }
        }

        return new ValidationResult(true, 0);
    }

Since I realized that when ValidationResult has first parameter false it does not changes the MyProperty property.

So, my goal is to somehow acknowledge, inside the ViewModel, is the ValidationResult true or false, so I can use that information inside my if statement. I could not find how to do this so far.

EldHasp
  • 6,079
  • 2
  • 9
  • 24
aca
  • 1,071
  • 5
  • 15
  • Do you need to get the ViewModel to know if there is any validation error in the TextBox? Or are you only interested in the error from your Validator, and the rest of the errors should be ignored? – EldHasp Feb 24 '23 at 14:27
  • TBH I'm not quite sure what are you asking, but i'll try to explain. I need to know when there is an error in my TextBox (which is visible on the UI), but I need that same information in the ViewModel, so I can stop the user from pressing a button if there is an invalid input inside my TextBox. – aca Feb 24 '23 at 14:29
  • 1
    You can't without referencing a control to obtain the actual Binding object. This is not recommended as it would introduce several issues. Instead you should implement property validation (instead of binding validation). It's very simple. You do it by [implementing the INotifyDataErrorInfo interface](https://stackoverflow.com/a/56608064/3141792) (the common companion of INotifyPrope4rtyChanged) on your data source/binding source, in your case the view model class. This way your view model is in full control over the validation. This is the recommended validation pattern in an MVVM environment. – BionicCode Feb 24 '23 at 15:45
  • The validation error state is shared by the entire UI element (in the general case, by DependencyObject). For example, you can define multiple validators in one binding, or define validator bindings for multiple properties. And an error in any validator will set the error state for the entire UI element. Do you need this condition? This task is relatively easy to solve. Or do you need to filter out all other validation errors and pass only the result of your "IntRangeRule" validator to the ViewModel? Here is a problem that is already quite difficult to solve. – EldHasp Feb 24 '23 at 15:45
  • Yes, you have identified the why validation rule exists. It is there to stop invalid result from going any further. If it would allow it to pass through then there would be no need for it as you could do the same in VM. So how does your if statement looks like in your VM? – XAMlMAX Feb 24 '23 at 22:57
  • @BionicCode Thanks, I will take a look at this. I'm working with some legacy code, so basically I just inherited this way of validation. – aca Feb 27 '23 at 09:03
  • @EldHasp I need the validation result of this specific property. – aca Feb 27 '23 at 09:05
  • @XAMlMAX With this way of validation, for example, if user types in `1-1`, the border of the textbox is set to red, to show an error, but I can still press the `Save` button, and the saved value will be `1`, because it was last successful result. If user types in `---`, I can `save` again, but the value will be `null`. – aca Feb 27 '23 at 09:08
  • So what you're saying is, that you need to validate it in VM and then refresh Save button command. If you want you can just throw an exception in property setter to have the same red border around text box. But I think your problem is to not allow null when pressing save? You need to look at validation on VM rather than ValidationRule and no INotifyDataErrorInfo is needed. – XAMlMAX Feb 27 '23 at 10:33
  • What you can do is to keep the binding validation (of you can't change it) and then in addition validate the properties of your view model class to handle ICommand.CanExecute. The binding validation will take care to show the error feedback. And the view model class takes care to let the e.g. save command's CanExecute return false (to disable the Button). Use the appropriate ValidationRule inside CanExecute (the same that the binding to that particular property users, but new insurance) to validate the property value. This extra validation is triggered by the property setter. – BionicCode Feb 27 '23 at 11:40

2 Answers2

-1

Validators like that raise a routed event when there is a validation.error and another when it is fixed. As you noticed there is no transfer of data from view to viewmodel.

There is is a routed event raised which you can handle in a parent. Say in a grid or at usercontrol level.

You obviously need a way to tell the viewmodel there's an error or not.

In your parent where you handle the event you can set a dependency property which is bound to a property from your viewmodel.

This is some old code intended to give you the idea. It might not cut and paste because it's old.

In the parent grid or usercontrol, or you can make it more re-usable by putting this sort of thing in the template for a contentcontrol. Then put your ui in that control.

The following uses a command.

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

Event to command is from mvvm light. I mentioned not being a cut and paste solution.

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

Note how it's hit when adding an error and when removing an error.

Routed event trigger.

// 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;
    }
}
Andy
  • 11,864
  • 2
  • 17
  • 20
-1

Here is a Behavior implementation to bind any property (including read-only).

using Microsoft.Xaml.Behaviors;
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
 
namespace CustomBehaviors
{
    /// <summary>To create a binding from any <see cref="DependencyProperty"/>,
    /// including "read-only".</summary>
    public class ReadOnlyBinding : Behavior<DependencyObject>
    {
 
        private bool isSeal;
        private DependencyProperty? _property;
        private BindingBase? _sourceBinding;
        public BindingBase? SourceBinding
        {
            get => _sourceBinding;
            set
            {
                if (isSeal)
                    throw new InvalidOperationException("Sealed!");
                _sourceBinding = value;
                if (value is null)
                {
                    BindingOperations.ClearBinding(this, SourceProperty);
                }
                else
                {
                    BindingOperations.SetBinding(this, SourceProperty, value);
                }
            }
        }
 
 
        public DependencyProperty? Property
        {
            get => _property;
            set
            {
                if (isSeal)
                    throw new InvalidOperationException("Sealed!");
                _property = value;
            }
        }
 
        /// <summary>Hidden Dependency Property.</summary>
        private static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register("Source", typeof(object), typeof(ReadOnlyBinding), new PropertyMetadata(null));
 
 
        protected override void OnAttached()
        {
            isSeal = true;
            if (Property is null)
                throw new NullReferenceException(nameof(Property));
 
            base.OnAttached();
 
            DependencyPropertyDescriptor
                .FromProperty(Property, AssociatedObject.GetType())
                .AddValueChanged(AssociatedObject, OnPropertyValueChanged);
            OnPropertyValueChanged(AssociatedObject, EventArgs.Empty);
        }
 
        private void OnPropertyValueChanged(object? sender, EventArgs e)
        {
            SetValue(SourceProperty, AssociatedObject.GetValue(Property));
        }
 
        protected override void OnDetaching()
        {
            base.OnDetaching();
            DependencyPropertyDescriptor
                .FromProperty(Property, AssociatedObject.GetType())
                .RemoveValueChanged(AssociatedObject, OnPropertyValueChanged);
        }
 
    }
}

Its use for your example:

        <TextBox ....>
            <i:Interaction.Behaviors>
                <cbs:ReadOnlyBinding  SourceBinding="{Binding SomePropertyOfViewModel, Mode=TwoWay}"
                                      Property="Validation.HasError"/>
            </i:Interaction.Behaviors>
            <TextBox.Text>
                <Binding Path="MyProperty"
                         UpdateSourceTrigger="PropertyChanged"
                         TargetNullValue="">
EldHasp
  • 6,079
  • 2
  • 9
  • 24