Bind my VM to Error Validation event or HasErrors Validation property

Sep 30, 2010 at 9:21 AM

I have a View form with binding.NotifyOnValidationError set to True at the elements of a form.

So if some autoconvertion fails the View is marked with HasErrors and fires and Error Event. Its is wonderful because not all validation errors are managed by IDataErrorInfo interface (the default error control of Caliburn.Micro).

My problem is I not have idea to make a Bind to my VM class for Validate.Error event or Element.Validate.HasErrors property.

I have a "Save" Button and I wan to define a  CanSave action method with return value equals to Validate.HasErrors value.

Could someone give me some trick .... I think I need to define some ElementConvention .... ?

Thank you in advance

 

P.D: Sorry for my poor English.

Sep 30, 2010 at 2:18 PM

It's an interesting question. Unfortunately I haven't got a ready (and proven effective) advice for this.
Off the top of my head, I would try this solution:

- define an interface to be implemented by thoese VMs willing to accept validation information from its corresponding view (let's call it IAcceptsErrorInfo)
- customize ConventionManager.ApplyValidation delegate to hook desired event from each UI element to VMs implementing IAcceptsErrorInfo
- in the VM, call NotifyOfPropertyChange("CanSave") when an event notification is received (or a state property is changed) through IAcceptsErrorInfo

Does it make sense?

Feb 3, 2011 at 8:05 PM

I scratched my head about this, too.

I couldn't find anything that works for WPF, which is really weird, because it this scenario should occur quite often.

What I came up with is an AttachedProperty, allowing to bind the View's ValidationsExceptions to a IList on the ViewModel.

 

 public class ValidationExceptionContext
{



public static readonly DependencyProperty ErrorsProperty =
DependencyProperty.RegisterAttached("Errors", typeof(IList<ValidationError>), typeof(ValidationExceptionContext),
new PropertyMetadata(null,ErrorsChanged));

public static IList<ValidationError> GetErrors(DependencyObject obj)
{
return (IList<ValidationError>)obj.GetValue(ErrorsProperty);
}

public static void SetErrors(DependencyObject obj, IList<ValidationError> value)
{
obj.SetValue(ErrorsProperty, value);
}

public static readonly DependencyProperty TrackExceptionsOnlyProperty =
DependencyProperty.RegisterAttached("TrackExceptionsOnly", typeof (bool), typeof (ValidationExceptionContext),
new FrameworkPropertyMetadata(true));
public static void SetTrackExceptionsOnly(DependencyObject d,bool value)
{
d.SetValue(TrackExceptionsOnlyProperty, value);
}

public static bool GetTrackExceptionsOnly(DependencyObject d)
{
return (bool) d.GetValue(TrackExceptionsOnlyProperty);
}

public static readonly DependencyProperty MarkEventsAsHandledProperty =
DependencyProperty.RegisterAttached("MarkEventsAsHandled", typeof (bool),
typeof (ValidationExceptionContext),
new FrameworkPropertyMetadata(true));

public static bool GetMarkEventsAsHandled (DependencyObject d)
{
return (bool) d.GetValue(MarkEventsAsHandledProperty);
}

public static void SetMarkEventsAsHandled(DependencyObject d, bool value)
{
d.SetValue(MarkEventsAsHandledProperty, value);
}


public static void ElementLoaded (object sender, RoutedEventArgs e)
{
var element = (FrameworkElement) sender;
element.Loaded -= ElementLoaded;
element.AddHandler(
System.Windows.Controls.Validation.ErrorEvent,
new RoutedEventHandler(ExceptionValidationHandler));
}


public static void ErrorsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
FrameworkElement element = (FrameworkElement)obj;
element.Loaded += ElementLoaded;
}

private static void ExceptionValidationHandler(object sender, RoutedEventArgs e)
{

            e.Handled = GetMarkEventsAsHandled(sender as DependencyObject );

            var args = e as System.Windows.Controls.ValidationErrorEventArgs;
if (args == null)
return;
if (!GetTrackExceptionsOnly( sender as DependencyObject )|| args.Error.RuleInError as System.Windows.Controls.ExceptionValidationRule != null)
{
ValidationError err = args.Error as ValidationError;
BindingExpression bindingInError = err.BindingInError as BindingExpression;
string dataItem = bindingInError.DataItem.ToString();
string property = bindingInError.ParentBinding.Path.Path;
string key = string.Concat(dataItem, ":", property);

if (args.Action == ValidationErrorEventAction.Added)
{
GetErrors(sender as DependencyObject ).Add(args.Error);
}
else { GetErrors(sender as DependencyObject).Remove(args.Error);
}
}

}
}

Its used like this:

<StackPanel ValidationExceptionContext.Errors="{Binding ValidationExceptions}">
     <TextBox Text={Binding MyProperty1, ValidatesOnExceptions=true, NotifyOnValidationError=True}
<TextBox Text={Binding MyProperty2, ValidatesOnExceptions=true, NotifyOnValidationError=True}

</StackPanel>
The ValidationExceptionContext should be used on an container containing the elements of interest, here I did it on a stackpanel, but it could be anything else (Listbox, UserControl...)

The ViewModel exposes an ObservableCollection<ValidationError> ValidationExceptions, by subscribing to its collectionChanged and counting the items the VM always knows if there is

an exception in the UI, thus being in an unsavable state despite the model possibly still valid because due to the exception the new value didn't get forwarded to it.

 

There are 2 more Properties (TrackExceptionsOnly, default=true), MarkEventsAsHandled (default=true)

by setting TrackExceptionsOnly to false the collection will also contain the regular Validation errors. By setting

MarkEventsAsHandled to false the ErrorEvent will continue to bubble up the tree.

 

When using with CaliburnConventionBinding you should replace the ApplyValidation Method in the ConventionManager, because Caliburn only sets ValidatesOnDataErrors by default.

I do it this way, by calling CaliburnCustomizer.CustomizeCaliburn() in my Bootstrapper

 public static class CaliburnCustomizer
    {
        public static void CustomizeCaliburn()
        {
            EnableValidationExceptionHandling();
        }

        private static void EnableValidationExceptionHandling()
        {
            Caliburn.Micro.ConventionManager.ApplyValidation =
                  new Action<Binding, Type, PropertyInfo>((binding, viewModelType, propertyInfo) =>
                  {
                      if (typeof(IDataErrorInfo).IsAssignableFrom(viewModelType))
                          binding.ValidatesOnDataErrors = true;
                      binding.NotifyOnValidationError = true;
                      binding.ValidatesOnExceptions = true;
                  });
        }
    }

 

Feb 4, 2011 at 7:20 AM

Many thanks.

The only solution I found before you subject is make a deep search of properties of WPF control ! but with my solution I lost the model independence. 

Feb 4, 2011 at 6:06 PM

I found a very similar solution in the meanwhile. It works almost by the same principle but is realized by an attached behaviour.

You can find it here (http://www.codeproject.com/KB/smart/WPF_Validation_Attribute.aspx?msg=3555243) , it looks less complicated than my solution.

You stil need to do the CaliburnCustomization described above.