Magellan Forms

Back to: Magellan Home

Data entry forms are common in line of business WPF applications, and they can become repetitive to write. Magellan includes a new set of controls that you can use to rapidly throw data entry forms together.

The goals of Magellan Forms are:

  • Minimal XAML
  • Abstract presentation concerns
  • Use conventions to infer as much information as possible
  • Flexible and extensible

Getting Started

To illustrate, take a form declared like this:

<Form>
    <Field For="{Binding Path=Server.Server}" />
    <Field For="{Binding Path=Server.CachedExchangeMode}" />
    <Field For="{Binding Path=Server.Username}" />
    <Field For="{Binding Path=Server.SecurityMode}" />
</Form>

The object model that this form is bound to looks like this:

public class ExchangeServerSettings : ServerSettings
{
    public string Server { get; set; }

    public string Username { get; set; }

    [DisplayName("Use cached Exchange mode")]
    public bool CachedExchangeMode { get; set; }

    [DisplayName("Security mode")]
    public ExchangeSecurityMode SecurityMode { get; set; }
}

public enum ExchangeSecurityMode
{
    [EnumDisplayName("Negotiate")]
    Negotiate,
    [EnumDisplayName("NTLM")]
    Ntlm,
    [EnumDisplayName("Kerberos")]
    Kerberos
}

This markup and code is all Magellan Forms needs to figure out how to render the form:

A WPF form. Notice how the checkbox and

Magellan was able to infer:

  • The caption for each field, using either the DisplayName attribute or property name.
  • The control to use for each field, based on the control type

Overriding

While Magellan Forms is able to infer settings based on the bindings, the feature is optional. You can instead manually set all of the field values:

<Field Header="First name:">
    <TextBox Text="{Binding Path=FirstName}" Width="200" />
</Field>

Fields are just ContentControls, so you can use anything you like inside a field. You can also choose to infer most of a field while overriding a specific setting, for example:

<Field Header="Surname:" For="{Binding Path=LastName}" />

Styling

A Form is simply an ItemsControl, which by default uses a StackPanel to lay out the children. A Field is a ContentControl. This means you can use styles and templates to customize how they are rendered.

For example, suppose our design team decided that all labels should now be right-aligned:

Now the labels are right-aligned

They just have to override the Field template in App.xaml:

<Style TargetType="Field">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Field">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="120" />
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>

                    <Label Content="{TemplateBinding Header}" HorizontalContentAlignment="Right" Margin="2" />
                    <ContentPresenter Grid.Column="1" />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Extending

The conventions used to infer control types and display settings can be customized through two extension points:

  1. IFieldConvention, used to figure out the heading and other settings
  2. IEditorStrategy, a list of which is used to create the appropriate editor for a type

You can implement IFieldConvention and then register it via the attached inherited property on a field:

<Form Form.FieldConvention="{x:Static MyFieldConvention.Instance}"> ...

When the default field convention is choosing an editor, it consults a list of registered IEditorStrategy implementations. There are three out of the box:

  1. TextBoxEditorStrategy
  2. ComboBoxEditorStrategy
  3. CheckBoxEditorStrategy

Writing your own is easy. For example, suppose you want to show a masked editor for numeric types. The code below assumes you use something like the Xceed masked editor:

public class MaskedTextBoxEditorStrategy : IEditorStrategy
{
    public object CreateEditor(FieldContext context)
    {
        var isInteger = context.PropertyDescriptor.PropertyType == typeof(int);
        var isDecimal = context.PropertyDescriptor.PropertyType == typeof(decimal);

        if (!isInteger && !isDecimal) 
        {   
            // This editor is only concerned with numeric types 
            return null;
        }

        var maskedTextBox = new MaskedTextBox();
        maskedTextBox.Mask = isInteger ? "999,999" : "999,999.99";
        BindingOperations.SetBinding(maskedTextBox, TextBox.TextProperty, context.FieldBinding);
        return maskedTextBox;
    }
}

The editor then just needs to be registered (usually somewhere in App.xaml.cs):

EditorStrategies.Strategies.Insert(0, new MaskedTextBoxEditorStrategy());

Note that when a field is inferred, all editors are consulted in order, and the first non-null result is used. That's why you typically want to Insert an editor to the top of the list rather than adding it to the end. I'm interested in feedback on this design.

Validation

Editor strategies can also make use of validation attributes to provide richer information and UI cues. For example, suppose you had a standard range of TextBox sizes:

<Style x:Key="TextBox.Small" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
    <Setter Property="Width" Value="100" />
</Style>

<Style x:Key="TextBox.Normal" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
    <Setter Property="Width" Value="200" />
</Style>

<Style x:Key="TextBox.Big" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
    <Setter Property="Width" Value="300" />
</Style>

The view model could use make use of Data Annotations to specify the maximum text length and whether a field is mandatory:

[Required]
[StringLength(30)]
public string Username { get; set; }

A custom editor strategy could detect these and set the TextBox settings as follows:

public class CustomTextBoxEditorStrategy : IEditorStrategy
{
    public object CreateEditor(FieldContext context)
    {
        if (context.PropertyDescriptor.PropertyType != typeof(string))
        {
            // We only deal with strings
            return null;
        }

        var required = context.PropertyDescriptor.Attributes.OfType<RequiredAttribute>().FirstOrDefault();
        var length = context.PropertyDescriptor.Attributes.OfType<StringLengthAttribute>().FirstOrDefault();

        if (required != null)
        {
            context.Field.IsRequired = true;
        }

        var textBox = new TextBox();
        BindingOperations.SetBinding(textBox, TextBox.TextProperty, context.Binding);
        if (length != null)
        {
            var max = length.MaximumLength;
            if (max < 20) textBox.Style = (Style)context.Field.FindResource("TextBox.Small");
            else if (max < 50) textBox.Style = (Style)context.Field.FindResource("TextBox.Normal");
            else textBox.Style = (Style)context.Field.FindResource("TextBox.Big");

            textBox.MaxLength = max;
        }
        return textBox;
    }
}

The editor strategy can also set values on the bindings, such as adding new ValidationRules.

Summary

Magellan Forms provides a foundation that allows you to leverage a little infrastructure code (editor strategies, styles, annotations) to gain a lot of reuse. I like to think of this as 'semantic XAML', that is, the functional XAML just describes that I want a form with some fields, rather than all the specifics about layout. The layout decisions are differed to styles that designers can manage, giving us consistency and a single point of change. I hope you find the feature useful.

Note for partial trust applications

Inference relies on invoking a private member on BindingExpression to work. This means it won't work in XBAP's and other partial trust scenarios. The rest of the forms library should work - just don't use the For property.

Thanks

I'd like to thank my colleague, Miguel Madero, who worked with me to figure out how inference would work.

Back to: Magellan Home

A picture of me

Welcome, my name is Paul Stovell. I live in Brisbane and work on Octopus Deploy, an automated deployment tool for .NET applications.

Prior to founding Octopus Deploy, I worked for an investment bank in London building WPF applications, and before that I worked for Readify, an Australian .NET consulting firm. I also worked on a number of open source projects and was an active user group presenter. I was a Microsoft MVP for WPF from 2006 to 2013.

20 Feb 2010

I tell you what Paul, I take my hat off to you, you are one of the smartest smart client devs I have ever seen, and the frameworks you create are highly innovative and really demonstrate what a good grasp you have on .NET.

100 out of 10 matey.

Massive respect to you.

20 Feb 2010

Paul, I have to echo what Sacha said mate. You really push things forward there mate, and I will be shamelessly ripping you off here because this will make some of our LOB apps so much more straightforward.

20 Feb 2010

Yeah, this is awesome Paul.

26 Feb 2010

This is top - just a quick question:

If I don't put a Form element on it appears to work. Looking in Reflector I can see some calls to Form.GetFieldBuilder. Am I setting myself up for a fall? The reason I'm not using a form is because I don't want Field items layed out in a StackPanel. I need a Grid for my custom layout.

BTW - I'm making some additional strategies which use DataAnnotation attributes to provide input validation. If you're interested I can drop the code to you.

Cheers,

Grae

26 Feb 2010

The Field control is standalone. The GetFieldBuilder is an attached DP. The idea was you might like to set a custom FieldBuilder for a field:

<Form>
    <Field Form.FieldBuilder="{Binding ...}" ... />
    <Field Form.FieldBuilder="{Binding ...}" ... />

But repeating that for every field might be annoying, so it's an inherited DP. But it's only necessary if you need to override the DP.

All of that said, if you want a different parent, you might be better overriding the Form style. Form is just an ItemsControl, so you can set an ItemsPanel instead with the layout of your choice. This keeps the 'semantic' goal of the Form.

Paul
Paul
02 Mar 2010

Assuming it doesn't work currently, how easy would it be to get this working under silverlight?

Valeriu Caraulean
Valeriu Caraulean
04 Mar 2010

The markup is looking really sweet. But how about design time, will this show up in VS designer as a label + text box?

<Field Header="Surname:" For="{Binding Path=LastName}" />
Valeriu Caraulean
Valeriu Caraulean
04 Mar 2010

I've looked briefly at sources and samples for Form.

From what I've seen there is no design time support for shortened markup. Writing the most of the XAML by hand myself I really appreciate ability to see the view/screen/form in VS designer.

<Field For="{Binding LastName}" />

Any chance to get design time support for this notation? XAML is looking nice, but I would really like to see the result in VS designer/Blend.

Anyway, thank you for a nice idea!

Daniel Walder
Daniel Walder
10 Mar 2010

I'm writing my bindings like this (using WPF validation with IDataErrorInfo):

<Field For="{Binding Value, Mode=TwoWay, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />

...and I was wondering why the validation was only updating when the textbox lost focus. Also, the red border that is generated for the validation error is being applied to the Field element, rather than it's child TextBox. I dug through the source and found a possible solution.

I added the following code into your BindingExtensions.Clone() method:

duplicate.UpdateSourceTrigger = originalBinding.UpdateSourceTrigger;
duplicate.ValidatesOnDataErrors = originalBinding.ValidatesOnDataErrors;
duplicate.ValidatesOnExceptions = originalBinding.ValidatesOnExceptions;

And included an empty template (just a placeholder) to the Field style in your generic theme on Validation.ErrorTemplate. This applies the validation to both the Field and it's TextBox, but the Field doesn't show any indication of having errors.

Really enjoying using the library, keep it up!