Nov 26 2009

Silverlight 3 access DataContextChanged event

In WPF when DataContext changes the DataContextChanged event fires. We could attach event handler to this event to execute some custom code.

In Silverligth 3 all is the same, except one small thing: DataContextCahnged is marked as internal… hmm... how then we could know what DataContext has changed?

We could do a simple trick. We can’t access DataContextChanged event, but we could bind some our dependency property to DataContext and handle dependency property changed event :)

Let’s build a small class which will do a binding for us.

public static class DataContextChangedEventManager
{
    public delegate void DataContextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args);

    public static readonly DependencyProperty DataContextChangedProperty =
        DependencyProperty.Register("DataContextChangedProperty", typeof(object), typeof(FrameworkElement), new PropertyMetadata(null, OnDataContextChanged));

    public static readonly DependencyProperty DataContextChangedEventHandlersProperty =
        DependencyProperty.Register("DataContextChangedEventHandlersProperty", typeof(EventHandler<DataContextChangedEventArgs>), typeof(FrameworkElement), new PropertyMetadata(null));

    public static void OnDataContextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var eventHandlers = obj.GetValue(DataContextChangedEventHandlersProperty) as EventHandler<DataContextChangedEventArgs>;
        if (eventHandlers != null)
        {
            eventHandlers.Invoke(obj, new DataContextChangedEventArgs(args.NewValue));
        }
    }

    ...
}

public class DataContextChangedEventArgs : EventArgs
{
    public DataContextChangedEventArgs(object value)
    {
        Value = value;
    }
    public object Value { get; private set; }
}

Here we are defining DataContextChangedProperty. This property will help us to react on DataContext changed event. When property has changed the OnDataContextChanged will be fired.

Also we have DataContextChangedEventHandlersProperty property defined which store event handlers.

Now let’s define method for attaching event handler for DataContextChanged event:

    ...

    public static void AddDataContextChangedEventHandler(this FrameworkElement control, EventHandler<DataContextChangedEventArgs> value)
    {
        if (control.GetValue(DataContextChangedEventHandlersProperty) == null)
        {
            control.SetValue(DataContextChangedEventHandlersProperty, value);
        }
        else
        {
            var eventHandler = Delegate.Combine((Delegate)control.GetValue(DataContextChangedEventHandlersProperty), value);
            control.SetValue(DataContextChangedEventHandlersProperty, eventHandler);
        }
        if (control.GetBindingExpression(DataContextChangedProperty) == null)
        {
            control.SetBinding(DataContextChangedProperty, new Binding());
        }
    }

    ...

And method for detaching event handlers:

    ...

    public static void RemoveDataContextChangedEventHandler(this FrameworkElement control, EventHandler<DataContextChangedEventArgs> value)
    {
        if (control.GetValue(DataContextChangedEventHandlersProperty) != null)
        {
            var eventHandler = Delegate.Remove((Delegate)control.GetValue(DataContextChangedEventHandlersProperty), value);
            control.SetValue(DataContextChangedEventHandlersProperty, eventHandler);
        }
        if (control.GetBindingExpression(DataContextChangedProperty) != null)
        {
            control.SetValue(DataContextChangedProperty, DependencyProperty.UnsetValue);
        }
    }

    ...

And the usage.

Lets say we have TextBlock and we want to show MessageBox when DataContext changes.

<TextBlock x:Name="TestBlock" DataContext="{Binding}"/>

We see here what there is binding defined on DataContext  propertyof the control. This binding means what we bind DataContext of the control to current control DataContext. Really this does nothing but we need it to get our DataContextChangedProperty binding to work.

And in code we need attach event handler for DataContext changed event.

public DataContextChanged()
{
    InitializeComponent();
    TestBlock.AddDataContextChangedEventHandler(OnDataContextChanged);
}

private void OnDataContextChanged(object sender, DataContextChangedEventArgs args)
{
    MessageBox.Show(args.Value.ToString());
}

And working example as usual:

The workarounds library with examples could be downloaded from here (VS2010): Andrew Veresov Workarounds library v1.0

Nov 21 2009

Silverlight 3 Validation Workaround

Silverlight 3 brings cool validation futures. But still it has some pitfalls. The main problem is the way how errors appear. Right now the only way we have to indicate error state of the control is to throw exception during data binding. Assume we have next data class:

public class DataClass
{
    private String _value;
    public String Value
    {
        get { return _value; }
        set { _value = value; }
    }
}

Lets add simple validation logic to it. We will checking for blank characters in a passed value. If blank characters are present then we will throw Exception with error description message.

public class DataClass
{
    private String _value;
    public String Value
    {
        get { return _value; }
        set
        {
            if (value.Length > 0 && value.IndexOf(' ') > 0)
            {
                throw new Exception("Value should not contain blank characters");
            }
            _value = value;
        }
    }
}

And if we have data binding defined:

<TextBox x:Name="exceptionInSetterBox" Text="{Binding Value, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"/>

then, if user enter a string with space characters the text box will take invalid state with error message in tooltip.

But what if you want manually set error state for particular control or perform async validation?

Well, every control has an Errors attached property. This property holds ReadOnlyObservableCollection<ValidationError> collection. Cool! We could create it by ourself and just set it throw SetValue method.

But... no :( we can't

The problem is in the ValidationError class. Simply it does not have public constructor available.

But, don't worry we will find workaround ;) (yes, ValidationError class also is marked as sealed, so we can't inherit from it)

If we can't instantiate ValidationError class then we could allow Silverlight to do it for us :)

The idea is to raise exception in some databound property of some Control (let's say ValidationErrors fabric) and grab ValidationErrors from there.

public class ValidationErrorBuilder: Control
{
    public static ReadOnlyObservableCollection<ValidationError> GetValidationErrors(String errorMessage)
    {
        ValidationErrorBuilder eb = new ValidationErrorBuilder();
        return GetErrors(eb, errorMessage);
    }

    private static readonly DependencyProperty ErrorProviderProperty =
        DependencyProperty.Register("ErrorProvider", typeof(ErrorProvider), typeof(ValidationErrorBuilder), new PropertyMetadata(null));

    private static ReadOnlyObservableCollection<ValidationError> GetErrors(Control control, string errorMessage)
    {
        var validationHelper = new ErrorProvider(errorMessage);

        control.SetBinding(ErrorProviderProperty, new Binding("ValidationError")
        {
            Mode = BindingMode.TwoWay,
            NotifyOnValidationError = false,
            ValidatesOnExceptions = true,
            UpdateSourceTrigger = UpdateSourceTrigger.Explicit,
            Source = validationHelper
        });

        control.GetBindingExpression(ErrorProviderProperty).UpdateSource();

        return (ReadOnlyObservableCollection<ValidationError>)control.GetValue(Validation.ErrorsProperty);
    }

    #region Nested type: ErrorProvider

    public class ErrorProvider
    {
        private readonly string _message;

        public ErrorProvider(string message)
        {
            _message = message;
            ThrowValidationError = true;
        }

        public bool ThrowValidationError { get; set; }

        [DebuggerNonUserCode]
        public object ValidationError
        {
            get { return null; }
            set
            {
                if (ThrowValidationError)
                {
                    throw new Exception(_message);
                }
            }
        }
    }

    #endregion
}

Now we could use ValidationErrorBuilder class for errors creation. To make picture complete let’s add extensions methods SetErrorState and ClearErrorState to all controls.

public static class ValidationExtender
{
    public static void SetErrorState(this Control control, string message)
    {
        control.SetValue(Validation.ErrorsProperty, ValidationErrorBuilder.GetValidationErrors(message));
        control.SetValue(Validation.HasErrorProperty, true);
        VisualStateManager.GoToState(control, "InvalidUnfocused", true);
        control.GotFocus += ControlGotFocus;
        control.LostFocus += ControlLostFocus;
    }

    static void ControlLostFocus(object sender, RoutedEventArgs e)
    {
        var control = sender as Control;
        if (control != null && (Boolean)control.GetValue(Validation.HasErrorProperty))
        {
            VisualStateManager.GoToState(control, "InvalidUnfocused", true);
        }
    }

    static void ControlGotFocus(object sender, RoutedEventArgs e)
    {
        var control = sender as Control;
        if (control != null && (Boolean)control.GetValue(Validation.HasErrorProperty))
        {
            VisualStateManager.GoToState(control, "InvalidFocused", true);
        }
    }

    public static void ClearErrorState(this Control control)
    {
        control.GotFocus -= ControlGotFocus;
        control.LostFocus -= ControlLostFocus;
        control.SetValue(Validation.ErrorsProperty, null);
        control.SetValue(Validation.HasErrorProperty, false);
        VisualStateManager.GoToState(control, "Valid", true);
    }
}

As result we will have:

Download validation library solution (VS 2010): AndrewVeresov.ValidationWorkaround v1.1