Robert Važan

Expose WPF controls to view models II

Access controls from within view model by adding a bit of code into window constructor. It's ugly, but it's sometimes necessary.

There are some controls in WPF that aren't designed for proper binding from view models, e.g. PasswordBox or the 3rd party Awesomium WebControl. These controls essentially blend data representation and UI in one class. That's bad design, of course, but these controls are a given and all we can do is to invent clever workarounds. There are many viable workarounds, which I will discuss briefly before presenting my favorite one liner.

NOTE: I no longer recommend this method. It relies on user controls, which are a serious performance killer in WPF. I have another simple technique that can be used anywhere in XAML code.

One can just instantiate the offending control in view model and then bind it down to the view via ContentControl. Aside from being totally non-MVVM, this technique has the downside of forcing all properties and children of the binded control to be set from within view model. We are really looking for some way to have the control in both view and view model.

Another simple solution, especially popular with PasswordBox, is to ship the control to view model as command parameter, e.g. when user presses Login button. One can even insert a converter in the command parameter binding to wrap the PasswordBox in an interface to comply with MVVM. This solution unfortunately breaks down rather quickly once you start doing anything non-trivial, for example trying to set initial value for the PasswordBox.

PasswordBox in particular already has a specialized solution via attached property (with many variations circulating around the net) that makes the Password property bindable. I am however looking for something more general-purpose. I tried to create general-purpose solution via attached property, but it failed in fringe cases due to some mysteries deep in WPF implementation. I am dismissing bindable wrapper controls for the same reason of being control-specific.

Here's my solution. I follow the convention that DataContext of UserControl is set to its view model. So I monitor the DataContext and upload the offending controls or their properties to whatever view model gets linked to the UserControl. This is all wrapped in a helper class, so all I have to do in the UserControl is to modify the constructor slightly:

public LoginWindow()
{
    InitializeComponent();
    ExposeControl<LoginViewModel>.Expose(this,
        view => view.Password,
        (model, box) => model.SetPasswordBox(box));
}

This code assumes that the PasswordBox control is named in XAML:

<PasswordBox x:Name="Password" />

When view model is attached to the view, its SetPasswordBox method is called with PasswordBox as a parameter. When it's detached from the view, the same method is called with null parameter to clear the association.

Note that the lambdas in the above example can contain arbitrary code, which means you can wrap the exposed control in an interface in order to comply with MVVM.

The above code relies on a little helper that looks like this:

public static class ExposeControl
{
    public static void Expose<TView, TModel, TValue>(
            TView control,
            Func<TView, TValue> selector,
            Action<TModel, TValue> writer,
            TValue zero = default(TValue))
        where TView : FrameworkElement
        where TModel : class
    {
        control.DataContextChanged += (sender, args) =>
        {
            var oldVM = args.OldValue as TModel;
            if (oldVM != null)
                writer(oldVM, zero);
            var newVM = args.NewValue as TModel;
            if (newVM != null)
                writer(newVM, selector((TView)sender));
        };
    }
}

public static class ExposeControl<TModel>
    where TModel : class
{
    public static void Expose<TView, TValue>(
            TView control,
            Func<TView, TValue> selector,
            Action<TModel, TValue> writer,
            TValue zero = default(TValue))
        where TView : FrameworkElement
    {
        ExposeControl.Expose<TView, TModel, TValue>(
            control, selector, writer, zero);
    }
}

You might wish to modify it a little. I am personally using a small variation of this code that works with UpdateControls wrapper classes.