by Paul Stovell via Paul Stovell on 2/20/2010 6:31:11 AM
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:
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:
Magellan was able to infer:
DisplayName
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:
Fields
ContentControls
<Field Header="Surname:" For="{Binding Path=LastName}" />
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.
Form
ItemsControl
StackPanel
Field
ContentControl
For example, suppose our design team decided that all labels should now be right-aligned:
They just have to override the Field template in App.xaml:
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>
The conventions used to infer control types and display settings can be customized through two extension points:
IFieldConvention
IEditorStrategy
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:
TextBoxEditorStrategy
ComboBoxEditorStrategy
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.
Insert
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:
TextBox
<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.
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.
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.
BindingExpression
For
I'd like to thank my colleague, Miguel Madero, who worked with me to figure out how inference would work.
Original Post: Magellan Forms
The content of the postings is owned by the respective author. CSharpFeeds is not responsible for the contents of the postings. This site is automatically generated and cannot be reviewed for abusive content. If you find abusive content on CSharpFeeds, please contact us. Designated trademarks and brands are the property of their respective owners. All rights reserved.