Robert Važan

Collection properties for Assisticant.Facades

New version of Assisticant.Facades lets you parameterize your WPF controls with collection dependency properties with the same ease as with scalars.

Assisticant.Facades provides for easy & clean authoring of WPF controls. Just declare your exposed properties, your computed properties, and the rest is straightforward XAML data binding. Unfortunately, Assisticant.Facades in its initial version only supported scalar dependency properties. I will now describe how to use updated Assisticant.Facades to expose collection-type dependency properties to control's view model.

We will create SelectMin control that displays minimum of several input numbers. These numbers will be provided to SelectMin via collection dependency property that can be declared in XAML and supports data binding. This is how our control will be used:

<sample:SelectMin>
    <sample:SelectMin.Inputs>
        <sample:MinInput Input="30" />
        <sample:MinInput Input="10" />
        <sample:MinInput Input="20" />
        <sample:MinInput Input="40" />
    </sample:SelectMin.Inputs>
</sample:SelectMin>

If everything goes smoothly, this control instance should render as "10". Not much, but it will serve nicely as our example. Note that Input property on every single MinInput tag can be data-bound. Whole SelectMin.Inputs property can be data-bound to an appropriately-typed collection as well.

Now, how do we implement this control? We will need several parts. Let's take a look at them one by one. Here's MinInput class along with MinInputCollection:

public class MinInput : Freezable
{
    internal readonly MinInputModel Model = new MinInputModel();

    public static readonly DependencyProperty InputProperty
        = DependencyProperty.Register(
        "Input", typeof(double), typeof(MinInput));
    public double Input
    {
        get { return (double)GetValue(InputProperty); }
        set { SetValue(InputProperty, value); }
    }

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

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

    protected override Freezable CreateInstanceCore()
    {
        return new MinInput();
    }
}

public class MinInputCollection
    : FreezableCollection
{
}

There are a couple things that deserve explanation. Firstly, notice how the Freezable trick is used on both classes to ensure that data binding works outside of logical tree. We have to define CreateInstanceCore() because of that, but that's not a big deal.

What's important is that we are calling Update() and UpdateAll() methods of Assisticant.Facades API. There's no Wrap() call like in facade controls, because our MinInput class is not visual. It has no XAML template and no OnApplyTemplate().

The next part of the puzzle is the SelectMin class.

public class SelectMin : Control
{
    readonly SelectMinModel Model = new SelectMinModel();

    public static readonly DependencyProperty InputsProperty
        = DependencyProperty.Register(
        "Inputs", typeof(MinInputCollection), typeof(SelectMin));
    public MinInputCollection Inputs
    {
        get
        {
            return (MinInputCollection)GetValue(InputsProperty);
        }
        set { SetValue(InputsProperty, value); }
    }

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

    public SelectMin()
    {
        Inputs = new MinInputCollection();
        FacadeModel.UpdateAll(Model, this);
    }

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

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

It's pretty standard facade control except it declares collection property. Notice how the constructor initializes the collection. This will allow us to omit collection declaration in XAML and add items to the pre-allocated collection instead.

Both SelectMin and MinInput declare models. These models are doing the actual work of calculating the minimum value. Let's take a look at them:

class MinInputModel
{
    public readonly Observable<double> Input
        = new Observable<double>();
}

class SelectMinModel
{
    public readonly ObservableList<MinInput> Inputs
        = new ObservableList<MinInput>();
    IEnumerable<MinInputModel> InputModels
    {
        get { return Inputs.Select(i => i.Model); }
    }
    public double Min
    {
        get
        {
            return InputModels.Any()
                ? InputModels.Min(m => m.Input.Value)
                : 0.0;
        }
    }
}

MinInputModel is trivial. SelectMinModel is more interesting. We can now see that Inputs property on SelectMin is mapped to Inputs property on SelectMinModel.

The collection type is different from the one in the control. Assisticant.Facades will copy the collection item by item. This is important, because we don't want to expose our view model's collection type to control consumers and we don't want to be forced to use WPF's collections, which won't work nicely with Assisticant.

Once we have MinInput controls in Inputs property, we still have one problem to solve. We are interested in MinInputModel, not the facade control. We handle this in InputModels property where we translate MinInput collection to MinInputModel collection.

Once that's done, it's trivial to compute the Min property. We can then use that property in control's XAML:

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

Voilà. Our control is complete. You can now create more sophisticated controls using Assisticant.Facades. If you are already using Assisticant.Facades, all you have to do is to upgrade the NuGet package to latest version.