May 30, 2014

Expose WPF controls to view model via attached property

WPF view models shouldn't access anything in the view, but there's one huge exception. Some complex controls combine conceptual view and conceptual model in a single physical class. Binding helps with properties, but how can view models access methods on such controls?

NOTE: I no longer recommend the technique described below. It has some unexplained interactions with WPF data binding logic, which results in mysterious null reference exceptions deep in framework code. I now use another generic technique to expose problematic controls to view models.

Take for example Awesomium's WebControl. It exposes GoBack() and many other methods that allow the application to control the embedded browser. Application using Awesomium likely has some WPF buttons that execute methods on view models via some ICommand. How can the view model execute methods on the WebControl?

Of course, one could consider this to be an example of bad control design. For example, the built-in Calendar control has SelectedDates property of type SelectedDatesCollection, which is essentially a model representing the state of the Calendar control and providing all the complex state-related methods like AddRange that cannot be exposed through simpler dependency properties. Nevertheless, third party libraries are a given and we have to deal with them somehow.

The solution I am offering is to map the whole half-view half-model control into view model property where it can be further manipulated by view model methods. How do we perform such mapping without messing around with code-behind? I've found an elegant way to do it with attached properties.

You just add one property to the view model:

public WebControl Browser { get; set; }

And then you add attached property to the XAML:

<awesomium:WebControl expose:ExposeControl.Path="Browser" .../>

The Path parameter is an arbitrary binding path relative to current DataContext. Support for more complex binding expressions is left as an exercise for the reader.

In order to make this all work, we have to define the attached property, which uses a little trick to get the control mapped to the view model as you can see below.

UPDATE: The code below contains a fix that ensures the binding continues to work after DataContext has changed. Plus another fix that prevents conversion errors showing up in binding logs.

public static class ExposeControl
{
    static readonly DependencyProperty SetterProperty
        = DependencyProperty.RegisterAttached("Setter",
        typeof(DependencyObject), typeof(ExposeControl));
    public static readonly DependencyProperty PathProperty
        = DependencyProperty.RegisterAttached("Path",
        typeof(string), typeof(ExposeControl),
        new PropertyMetadata(null, Expose));
    public static string GetPath(DependencyObject obj)
    {
        return (string)obj.GetValue(PathProperty);
    }
    public static void SetPath(DependencyObject obj, string value)
    {
        obj.SetValue(PathProperty, value);
    }
    static void Expose(DependencyObject obj,
                       DependencyPropertyChangedEventArgs args)
    {
        var binding = new Binding((string)args.NewValue)
        {
            Mode = BindingMode.OneWayToSource,
            Converter = new NoopConverter(),
            FallbackValue = obj
        };
        BindingOperations.SetBinding(obj, SetterProperty, binding);
    }

    class NoopConverter : IValueConverter
    {
        public object Convert(object value, Type targetType,
            object parameter, CultureInfo culture)
        {
            return value;
        }
        public object ConvertBack(object value, Type targetType,
            object parameter, CultureInfo culture)
        {
            return value;
        }
    }
}

And that's it. Enjoy.

No comments:

Post a Comment