A place for spare thoughts

21/05/2013

WPF UI Validation technique

Filed under: c#, wpf — Ivan Danilov @ 13:51

WPF provides several methods of validation, most common are implementing IDataErrorInfo on ViewModel, implementing custom ValidationRule, use ValidatesOnException whatever.

But for MVVM pattern really only IDataErrorInfor is good. The problem with other approaches – well, the VM can’t know with them if error happened. Yeah, user gets error indication, error is handled… but the VM thinks everything is good. Suppose VM controls if OK button is enabled or disabled (e.g. via CanExecute of bound command) – how could VM know when to disable the button if it doesn’t know if error happened?

Let’s try to imagine absolutely trivial example: window with TextBox and a Button. User should type integer in the TextBox and click the button. If user doesn’t enter a number at all or entered some invalid value (like ‘abc’ or non-integer like ‘1.23’) – button should be disabled and TextBox should have normal error notification (default WPF red outline is ok).

The task is trivial, so the code should better not be complicated as well. Of course we want MVVM-ish thing, so let’s write a VM:

class VM
{
    public int Value { get; set; }
    public ICommand Ok { get; set; }
}

VM most probably needs to implement INotifyPropertyChanged for its properties, but it is not the case here, so I’ve omitted these. Can you see the problem already? Value has int type. When user types ‘abc’ in the TextBox – Value won’t receive changes until value becomes parseable integer again. Binding just can’t pass them to VM – it has no means to do so. And thus we can’t notify Ok command that it can’t be executed right now. Moreover, if OK button is supposed to save something… imagine a scenario: user entered 100 to the TextBox, this value has propagated to Value property, then user accidentaly typed ‘o’ (letter o) instead of ‘0’ (zero) – they are pretty close on my keyboard – and clicked OK. Oops. We just overwritten old data with 100, when user thought it will be 1000. Definitely not good.

So, what is the solution? One apparent one is to change Value’s type: let it be a string. We can parse it in the VM and determine if it is correct, and disable Ok command. It will work, of course, but if you have two dozens of such pseudo-integers – VM becomes a pile of ToString() and Int32.Parse() calls here and there, which hides all the logic. A mess, shortly said.

What is the problem we’re trying to solve, exactly? Inability of View to notify ViewModel that error has happened. Ok. Lets pretend we found a place in the View that knows about the problem. How could it notify the VM? Well, View has very precise knowledge about its VM – it is the DataContext. But the DataContext is of type object… making View know precise type of its VM is probably not a good idea (besides it will make the solution require separate modification for each case), so we will encapsulate details of error notifications in the interface:

    public interface IUIValidationErrorSink
    {
        void AddUIValidationError(string propertyPath, string errorContent);
        void RemoveUIValidationError(string propertyPath);
    }

Don’t be amused where this precise arguments are came from – it will become clear after a couple of minutes. So, if our validation mechanism could get instanse of IUIValidationErrorSink object – it could notify interested parties that new error appeared or existing error is gone. So far so good.

VM will implement this in very straightforward way:

class VM : IUIValidationErrorSink
{
    private Dictionary<string, string> _uiErrors = new Dictionary<string, string>();

    public VM()
    {
        Ok = new RelayCommand(DoOk, CanOk); // RelayCommand is from famous John Smith's MVVM article here: http://msdn.microsoft.com/en-us/magazine/dd419663.aspx#id0090051
    }

    public int Value { get; set; }
    public ICommand Ok { get; set; }

    private void DoOk()
    {
        // Do something useful
    }

    private bool CanOk(object _)
    {
        return _uiErrors.Empty();
    }

    void IUIValidationErrorSink.AddUIValidationError(string propertyPath, string errorContent)
    {
        UIErrors[propertyPath] = errorContent;
    }

    void IUIValidationErrorSink.RemoveUIValidationError(string propertyPath)
    {
        UIErrors.Remove(propertyPath);
    }
}

Another problem is how we would find which control’s DataContext we should take? For example, if you have ItemsControl of something editable – each item has its own DataContext.

To answer this question we need to know a bit about WPF’s Binding Validation mechanism. It is implemented via Routed Events, particularly ErrorEvent in the System.Windows.Controls.Validation class (if you want to know more about Routed Events – you may start here). ErrorEvent is raised in the WPF Binding mechanism whenever error appears or disappears (actually, Binding calls Validation.AddValidationError and Validation.RemoveValidationError methods, which in turn call RaiseEvent). The target of this routed event is the DependenencyObject that owns bound DependencyProperty. As this event has Bubbling strategy, it then re-raised for each parent control in the Visual Tree from target until either the root is reached or someone marks event as Handled.

If only we could set routed event handler for ErrorEvent somewhere near the top of the View’s visual tree, where we always know our ViewModel… The good news are that we can – attached properties make this quite easy:

        public static readonly DependencyProperty IsUIValidationErrorSinkProperty =
            DependencyProperty.RegisterAttached("IsUIValidationErrorSink",
                                                typeof (bool),
                                                typeof (UIValidation),
                                                new PropertyMetadata(false, IsErrorSinkPropertyChangedCallback));

        public static bool GetIsUIValidationErrorSink(DependencyObject obj)
        {
            return (bool) obj.GetValue(IsUIValidationErrorSinkProperty);
        }

        public static void SetIsUIValidationErrorSink(DependencyObject obj, object value)
        {
            obj.SetValue(IsUIValidationErrorSinkProperty, value);
        }

        private static void IsErrorSinkPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var control = dependencyObject as FrameworkElement;
            if (control == null)
                throw new ArgumentException("IsUIValidationErrorSink property could be attached only to FrameworkElement descendants");

            control.AddHandler(Validation.ErrorEvent, HandleErrorEvent);
        }

Here we restrict the property to FrameworkElement because we need access to FrameworkElement.DataContext. Assuming we have written UIValidationRule already (see below), our handler might look like this:

        private static void HandleErrorEvent(object sender, RoutedEventArgs e)
        {
            var args = (ValidationErrorEventArgs) e;
            if (!(args.Error.RuleInError is UIValidationRule)) return;
            
            var sinkControl = (FrameworkElement) sender;
            var sink = sinkControl.DataContext as IUIValidationErrorSink;
            if (sink == null)
            {
                PresentationTraceSources.DataBindingSource.TraceEvent(TraceEventType.Error, 0, "UIValidation.IsUIValidationErrorSink attached property's HandleError encountered an error: DataContext does not implement IUIValidationErrorSink");
                return;
            }

            // filter out rules that are not UiValidationRule - we're handling only these
            var error = Validation.GetErrors((DependencyObject)args.OriginalSource)
                .FirstOrDefault(err => err.RuleInError is UIValidationRule);
            
            var bindingExpression = (BindingExpression) args.Error.BindingInError;
            string propertyPath = bindingExpression.ParentBinding.Path.Path;

            if (error == null)
            {
                sink.RemoveUIValidationError(propertyPath);
            }
            else
            {
                var errorMessage = (string) error.ErrorContent;
                sink.AddUIValidationError(propertyPath, errorMessage);
            }

            args.Handled = true;
        }

Now we need to write last piece – UIValidationRule which will actually validate the input. The goal here is to make it as easy to use and extensible as possible: first because we don’t won’t to use clumsy long XAML code pieces and second because we may want to write many slightly different rules: for ints, doubles, DateTimes etc. Also we want error messages to be customizable and localizable. Hence UIValidationRule will be an abstract base class which will be extended then.

My first idea was to make one descendant of this abstract class for each rule I needed. Though it turned out these descendants were pretty much the same and had almost everything duplicated – the only real difference was use of Int32.TryParse vs Double.TryParse. So I’ve decided to have single descendant of UIValidationRule which takes something akin to TryParse delegate as an argument:

    public abstract class UIValidationRule : ValidationRule
    {
        public string DefaultErrorMessage { get; set; }
    }

    public sealed class RelayUIValidationRule : UIValidationRule
    {
        private readonly Func<string, CultureInfo, string> _validator;

        /// <param name="validator">Mapper from string value to error or null if validation succeeded</param>
        public RelayUIValidationRule(Func<string, CultureInfo, string> validator)
        {
            _validator = validator;
        }

        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            var str = (string) value;
            string err = _validator(str, cultureInfo);
            return err == null ? ValidationResult.ValidResult : new ValidationResult(false, err);
        }
    }

Here Func<string, CultureInfo, string> takes string representation and culture info and returns null if validation succeeds and not null string with error message otherwise.

Also ‘normal’ way of using binding validation rules usually looks like this:

<TextBox>
    <Binding Path="Value" NotifyOnValidationError="True">
        <Binding.ValidationRules>
            <local:MyBindingValidationRule />
        </Binding.ValidationRules>
    </Binding>
</TextBox>

Ahem, really? 7 lines of XAML code to bind one value, not mentioning boilerplate NotifyOnValidationError="True"? No-no-no, it is totally unacceptable. Lets see if we could find a better, more terse way. Actually it is not that hard, if we assume control can’t have two UI validation rules at once (hey, this poor string shouldn’t be parsable as double and DateTime at the same time!):

    public class ValidatedBinding : Binding
    {
        private UIValidationRule _uiValidation;

        public ValidatedBinding()
        {
            NotifyOnValidationError = true;
            UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
        }

        public UIValidationRule UIValidation
        {
            get { return _uiValidation; }
            set
            {
                if (_uiValidation != null)
                    ValidationRules.Remove(_uiValidation);
                _uiValidation = value;
                if (_uiValidation != null)
                    ValidationRules.Add(_uiValidation);
            }
        }
    }

And final step: make instantiation of validation rules driven by MarkupExtensions:

    internal delegate bool TryParseFunc<T>(string source, CultureInfo cultureInfo, out T result);

    public abstract class UIValidationExtensionBase<T> : MarkupExtension
    {
        private readonly string _customErrorMessage;
        private readonly TryParseFunc<T> _tryParseFunc;

        internal UIValidationExtensionBase(string customErrorMessage, TryParseFunc<T> tryParseFunc)
        {
            _customErrorMessage = customErrorMessage;
            _tryParseFunc = tryParseFunc;
        }

        /// <summary>
        /// Specifies that empty string and null values should be allowed by the validation mechanism. 
        /// False by default.
        /// </summary>
        public bool AllowNulls { get; set; }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            Func<string, CultureInfo, string> validation =
                (s, c) =>
                    {
                        if (AllowNulls && String.IsNullOrEmpty(s))
                            return null;
                        T result;
                        bool success = _tryParseFunc(s, c, out result);
                        return success ? null : _customErrorMessage;
                    };

            return new RelayUIValidationRule(validation)
                       {
                           DefaultErrorMessage = _customErrorMessage
                       };
        }
    }

    [MarkupExtensionReturnType(typeof (UIValidationRule))]
    public class IntUIValidationExtension : UIValidationExtensionBase<int>
    {
        public IntUIValidationExtension()
            : this("Incorrect number format")
        {
        }

        public IntUIValidationExtension(string customErrorMessage)
            : base(customErrorMessage, (string s, CultureInfo c, out int result) => int.TryParse(s, NumberStyles.Integer, c, out result))
        {
        }
    }

    [MarkupExtensionReturnType(typeof (UIValidationRule))]
    public class DateTimeUIValidationExtension : UIValidationExtensionBase<DateTime>
    {
        public DateTimeUIValidationExtension()
            : this("Incorrect date/time format")
        {
        }

        public DateTimeUIValidationExtension(string customErrorMessage)
            : base(customErrorMessage, (string s, CultureInfo c, out DateTime result) => DateTime.TryParse(s, c, DateTimeStyles.None, out result))
        {
        }
    }

    // I think you could come up with other extensions as well 🙂

And finally, the first clumsy syntax with 7 lines transformed into beautiful one-liner:

<TextBox Text="{WpfValidation:ValidatedBinding UIValidation={WpfValidation:IntUIValidation}, Path=Value" />

Well, don’t forget to put UIValidation.IsUIValidationErrorSink="True" somewhere in your XAML.

Advertisements

Create a free website or blog at WordPress.com.