Introducing WPF-Forge – Implementing custom controls in Forge.Forms (ex WpfMaterialForms)

I had planned on writing follow up tutorials on app development in WPF, but due to a demanding job and limited time I couldn’t keep up to that task. I will try to post more regularly from now on.

Today, I am happy to announce that in collaboration with Github user redbaty, we started working on a new project: WPF-Forge (inspired by “FORms GEneration”).

The idea behind this project is to include a family of tools that will aid in WPF development. Features will be split in small packages, in an effort to make things simpler, and to motivate new contributors.

To achieve this, we are moving current package WpfMaterialForms into Forge.Forms. Similarly, Material.Application will be split and moved to Forge.App/Forge.Routing, and so on.

MaterialForms has never had a proper getting started guide. The new packages will be documented much more thoroughly. But, because many features are likely to change, I cannot write a proper tutorial yet. For this reason, I am writing a guide that references the old structure of MaterialForms. 95% of this guide will be applicable to Forge.Forms, with maybe a few naming differences.

A question that has been asked the most is how to create custom controls. To answer that, I will implement a progress bar control in this post. This knowledge will easily translate to all other controls you may need in your project.

To create a custom control, we must first ask: Can we display this control just by looking at the type, or do we need some special modifier in its attributes?

For example, if I want to implement a check box control, we would display it when we see Boolean type. We would never need to consider displaying a check box for a DateTime. When I think of a date picker control, a DateTime type comes to mind. For these kind of controls we will be using a TypeBuilder (more on that shortly).

If we are representing a type, it means that the information to create its control is stored implicitly inside the type. Whenever we see a string, our first instinct is to display a text box.

Some questions may not have a direct answer for the property type they should represent. For example, I want to be able to pick one element from a list of elements. A combobox solves this problem, but we cannot immediately tell what kind of type the selection or the list must have. It could be a list of strings, a list of numbers, a list of dates, etc. Also, we cannot know that a property should be selectable from a list just by looking at its type (though enums could have this quality).

When we need this selection behavior, we decorate the property with a [SelectFrom("{Binding MyList}"], which will display a different control from what we would expect (a string would display a text box if we didn’t include this annotation). For these kind of properties we hook a PropertyBuilder.

Some fields may be a combination of both. For example, when we want to display booleans as switches instead of check boxes, we would decorate the boolean property with a [Switch] attribute. Because both check box and switch are applicable only to a boolean, the switch control would be created from a type builder. We wouldn’t need a property builder because a [Switch] attribute would not make sense in any other type. Such a builder would check for the SwitchAttribute and make its decision based on the presence of that attribute.

If we wanted to build the switch from a property builder, it would have to check if the property is a boolean, and only then it would return the switch control. This type constraint is better captured implicitly in type builders.

In FormBuilder, PropertyBuilders are queried first to see if the property has any attribute that modifies the generated field. If no such builder takes responsibility then TypeBuilders are checked, which return fields for types like string and boolean.

Now that we have an understanding of the two types of builder hooks, we ask ourselves:

  • What type would a progress bar represent? Answer: Numeric types.

  • Do we need a special modifier for a progress bar? Answer: Yes, because by default we would not want our numbers to display as progress.

From these answers we see that we are in our [Switch] situation, a progress bar is applicable only to numeric types, but we also need a decorator to indicate that this control is special and not implicit.

Since there are many numeric types, it is easier to register it as a property builder. We suppose that the user will see no reason to put a [Progress] in a string. The builder will check if our property has the [Progress] attribute. If yes, then we return a ProgressField, otherwise we return null, which tells the system that we are passing and it should check other builders.

The builder will look like this:

class ProgressBuilder : IFieldBuilder
{
    public FormElement TryBuild(IFormProperty property, Func<string, object> deserializer)
    {
        var attribute = property.GetCustomAttribute<ProgressAttribute>();
        return attribute != null
            ? new ProgressField(property.Name, property.PropertyType)
            {
                Maximum = Utilities.GetResource<object>(attribute.Maximum, 100d, Deserializers.Double)
            }
            : null;
    }
}

We check for the attribute, if it exists we return a ProgressField. The reason why I’m defaulting to a double for maximum is that it’s the type used by WPF progress bars. Also notice the parse type is object in order to allow ints and other numeric types. If the user gives some weird value here they’re in for a crash! It’s better than crashing on [Progress(Maximum = 100)] though, because 100 is not a double, which would leave the user scratching their head.

We hook this attribute to FormBuilder PropertyBuilders. Since this is will be a library default, I’m modifying the FormBuilder code, otherwise you would inject your builders in initialization code.

You would insert them like this:

FormBuilder.Default.PropertyBuilders.Insert(0, new ProgressBuilder());

or you would have a factory to create a new FormBuilder:

var builder = new FormBuilder();
builder.PropertyBuilders.Insert(0, new ProgressBuilder())
return builder;

The order of insertion is important, because another builder may create something else before we have the chance to evaluate the property.

Because this builder operates only when the property has the specific [Progress] attribute, we can safely insert it first. In cases when your builder is more generic you would consider its order of execution compared to other builders.

What I’ve done is that I added this in the FormBuilder source code:

PropertyBuilders = new List<IFieldBuilder>
{
    // Default property builders.
    new ProgressBuilder(),
    new SelectFromBuilder(),
    new SliderBuilder()
};

You can notice that by the time of writing this post, there is a combo box interceptor, and a slider builder. The slider builder used the same reasoning as the progress; it is easier to include the builder for all numeric properties, and let the user decide when they want it.

Now, since we have the building in place, we implement the ProgressField and the displayed control.

The following code handles the field setup and the presenter control. Presenters are lookless by default. You will find that this boilerplate is repeated for every field type.

public class ProgressField : FormField
{
    private readonly string key;

    public ProgressField(string key)
    {
        this.key = key;
    }

    public IValueProvider Maximum { get; set; }

    protected internal override void Freeze()
    {
        base.Freeze();
        Resources.Add("Value", new PropertyBinding(key, false));
        Resources.Add(nameof(Maximum), Maximum ?? new LiteralValue(100d));
    }

    protected internal override IBindingProvider CreateBindingProvider(IResourceContext context, IDictionary<string, IValueProvider> formResources)
    {
        return new ProgressPresenter(context, Resources, formResources);
    }
}

public class ProgressPresenter : ValueBindingProvider
{
    static ProgressPresenter()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ProgressPresenter), new FrameworkPropertyMetadata(typeof(ProgressPresenter)));
    }

    public ProgressPresenter(IResourceContext context,
        IDictionary<string, IValueProvider> fieldResources,
        IDictionary<string, IValueProvider> formResources)
        : base(context, fieldResources, formResources, true)
    {
    }
}

In XAML, we define a look for this control. I’m using material design for this demonstration. After declaring the control, I register it in the Material.xaml theme dictionary.

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:defaults="clr-namespace:MaterialForms.Wpf.Fields.Defaults"
    xmlns:fields="clr-namespace:MaterialForms.Wpf.Fields">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ProgressBar.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <ControlTemplate x:Key="MaterialProgressPresenterTemplate" TargetType="{x:Type defaults:ProgressPresenter}">
        <Grid>
            <ProgressBar
                x:Name="ValueHolderControl"
                Maximum="{fields:FormBinding Maximum}"
                Style="{StaticResource MaterialDesignLinearProgressBar}"
                ToolTip="{fields:FormBinding ToolTip}"
                Value="{fields:FormBinding Value}" />
        </Grid>
    </ControlTemplate>

    <Style x:Key="MaterialProgressPresenterStyle" TargetType="{x:Type defaults:ProgressPresenter}">
        <Setter Property="Margin" Value="8,16" />
        <Setter Property="HorizontalAlignment" Value="Stretch" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="Template" Value="{StaticResource MaterialProgressPresenterTemplate}" />
    </Style>

</ResourceDictionary>

For a quick test I added this class:

class ProgressModel
{
    [Text("Loading...")]

    [Field, Progress]
    public double Progress => 60d;
}

Of course you would have to update your progress value in real scenarios, but a fixed value is enough for us to check if it looks pretty:

progress.png

This control needs more features, such as a percentage, indeterminate progress support, etc. But I’m leaving it here for now.

Thanks for reading!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s