Robert's blog
Robert Važan

Easy WPF control authoring with Assisticant.Facades

Gone are the days of painstakingly defining controls with dependency properties and event handlers. You can now use Assisticant in custom controls.

Assisticant is a breath of fresh air for all WPF programmers. There's no longer any need for event handlers and other stone age WPF concepts. Everything is just data binding and commands. Until, well, the time comes to design some custom control for your project. Vanilla Assisticant won't help you here and you are back to the messy event handlers. That's why I created Assisticant.Facades, an extension to Assisticant that makes control authoring a breeze.

Let's say we want to create new custom control library project that will include only one custom control. This custom control could do fancy things, but for the sake of simplicity, our custom control will merely display full name when given first and last name and an option to reverse their order. It will be used like this:

<sample:FullName First="Robert" Last="Važan" IsReversed="False" />

It is apparent the control will have to do some internal processing to display the full name. This could be done in XAML with data triggers and formatters, but that approach will quickly fail with increasing complexity of control behavior.

Another approach is to hook First, Last, and IsReversed properties with event handlers that recalculate the full name and write it out dynamically into element tree. That's what's usually done by control authors. It's tedious and messy. And totally unnecessary, because Assisticant.Facades now makes it a child's play to implement complex control behavior with no events at all.

We will start by defining our custom control:

public class FullName : Control
{
    public static readonly DependencyProperty FirstProperty
        = DependencyProperty.Register(
            "First", typeof(string), typeof(FullName));
    public string First
    {
        get { return (string)GetValue(FirstProperty); }
        set { SetValue(FirstProperty, value); }
    }

    public static readonly DependencyProperty LastProperty
        = DependencyProperty.Register(
            "Last", typeof(string), typeof(FullName));
    public string Last
    {
        get { return (string)GetValue(LastProperty); }
        set { SetValue(LastProperty, value); }
    }

    public static readonly DependencyProperty IsReversedProperty
        = DependencyProperty.Register(
            "IsReversed", typeof(bool), typeof(FullName));
    public bool IsReversed
    {
        get { return (bool)GetValue(IsReversedProperty); }
        set { SetValue(IsReversedProperty, value); }
    }

    static FullName()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(FullName),
            new FrameworkPropertyMetadata(typeof(FullName)));
    }
}

Pretty standard. Now we will do something new and unexpected. We will define view model for our custom control. Don't panic. This is an internal view model that will never be seen by users of our control. Users will happily bind to the First, Last, and IsReversed dependency properties we have exposed. We will use the view model internally to compute the full name in a clean and straightforward way. This is what it looks like:

class FullNameModel
{
    public readonly Observable<string> First
        = new Observable<string>();
    public readonly Observable<string> Last
        = new Observable<string>();
    public readonly Observable<bool> IsReversed
        = new Observable<bool>();

    public string Full
    {
        get
        {
            var first = First.Value ?? "";
            var last = Last.Value ?? "";
            return !IsReversed.Value
                ? first + " " + last
                : last + " " + first;
        }
    }
}

The logic of the view model is straightforward, but where do we set First, Last, and IsReversed properties? Well, this is done automatically for us by Assisticant.Facades.

Assisticant.Facades will sync every dependency property on the control to same-named property/field/Observable<> on the view model. In order to make this sync work, we have to add one field and three short methods to the FullName class. These parts look alike for all controls, so you can just copy them around:

readonly FullNameModel Model = new FullNameModel();

public FullName()
{
    FacadeModel.UpdateAll(Model, this);
}

public override void OnApplyTemplate()
{
    FacadeModel.Wrap(Model, GetTemplateChild("Root"));
    base.OnApplyTemplate();
}

protected override void OnPropertyChanged(
    DependencyPropertyChangedEventArgs args)
{
    base.OnPropertyChanged(args);
    FacadeModel.Update(Model, this, args);
}

This will turn our control into a facade - dependency property frontend for our internal MVVM implementation. Calling FacadeModel.Update() syncs one dependency property into corresponding model property. Calling FacadeModel.UpdateAll() syncs all of them. It is possible to sync scalar properties as well as collections. FacadeModel.Wrap() is a comfort wrapper around ForView.Wrap() that is easy to use in OnApplyTemplate(). It will do casting and a null check for you.

Finally, we have to create some default style for our control and place it in Generic.xaml of the control library. The "Root" element will have its DataContext set to control's view model in OnApplyTemplate(). That's why the Label element can simply data-bind to the Full property on the view model.

<Style TargetType="sample:FullName">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="sample:FullName">
                <Grid x:Name="Root">
                    <Label Content="{Binding Full}" />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And that's it. Now you can build arbitrarily complex custom controls with clean and simple view models. I've built JungleControls library this way. Now get the NuGet package or the source code and start building your own controls. See also my tutorial on collection properties with Assisticant.Facades.