The only difference in these form screen shots is the applied style.


Simple & Clean XAML
The below XAML is almost void of layout or positioning XAML markup. This allows almost all form layout to be controlled by applying a style; as it should be. If I had chosen to put the header Image outside the form, there would almost be no XAML
This looks simple and clean because it is.
<pt:Form x:Name="formMain" Style="{DynamicResource standardForm}" Grid.Row="1">
<pt:FormHeader>
<pt:FormHeader.Content>
<StackPanel Orientation="Horizontal">
<Image Source="User.png" Width="24" Height="24" Margin="0,0,11,0" />
<TextBlock VerticalAlignment="Center" Text="General Information" FontSize="14" />
</StackPanel>
</pt:FormHeader.Content>
</pt:FormHeader>
<TextBox pt:FormItem.LabelContent="_First Name" />
<TextBox pt:FormItem.LabelContent="_Last Name" />
<TextBox pt:FormItem.LabelContent="_Phone" Width="150" HorizontalAlignment="Left" />
<CheckBox pt:FormItem.LabelContent="Is _Active" />
</pt:Form>
Background
A month ago I was thinking about the work required to lay out a Line of Business (LOB) Application form and the maintenance of these forms. Developer scenarios like inserting rows of controls in the middle of a large Grid layout and the extra work required to perform this task. The work required to accomplish these everyday tasks was part of the motivation for XAML Power Toys.
In today’s WPF LOB forms the Grid is normally used for form layout. Typically a UI control has an associated Label that is located in a nearby Grid column or Grid row.
When a developer needs to move a UI control the corresponding Label must also be moved. Two distinct actions are required to move both controls.
This scenario gets more complex when attempting to move the controls using a GUI designer when the Grid rows or Grid columns have their Height or Width set to Auto. The GUI layout engine tries its best by adding or adjusting a Margin property to maintain size and alignment. As developers we typically then remove the designer applied Margins if our Grid sizing mode is auto.
When laying out the form and the Grid sizing is Auto, all the rows and columns are all initially collapsed since all cells are empty. This makes it necessary to start in the XAML editor or add sizes to Grid rows and columns until they have content.
I went home one night and wrote the Alpha version of the solution presented here. Then Dr. WPF and I sat down together worked out some kinks and added goodness. I spent another few hours hammering on this and wrote up the demos.
I would greatly appreciate your feedback on this blog post, especially if you are doing WPF LOB development.
Putting a Square Peg in Round Hole
The root of the problem is not the GUI designers or the WPF layout controls.
What is required is the ability to layout and move a single control that renders the UI control and corresponding Label.
Let me illustrate. If you had a requirement to to place 7 CheckBoxes on the form, stacked up, you would add a StackPanel to the form, select the StackPanel in the WPF Designer and double click on the CheckBox control in the Toolbox 7 times, set a few properties and your done. If you had to reorder these, it would take 1-2 seconds, done. No fussing about.
Today we pay developers to move controls around on the screen while fighting with layout controls that were not designed to allow the simultaneous movement of two controls easily. Our layout controls do not facilitate the laying out of controls in an Auto sized, Auto layout mode. I think that software companies would rather have that time back so their developers can learn new technical skills like MVVM, write Unit Tests and deliver more robust applications to customers. I’m not implying that these tasks are overly difficult, just time consuming, requiring extra steps. It’s a simple question of economics. How do you want your developers and designers spending their valuable time?
Solutions
We need a solution that:
- enables RAD GUI layout and maintenance of LOB forms
- is toolable
- leverages the intrinsic power of WPF & Silverlight
- can be styled
- lays itself out
- responds well to localization
First Choice – Enhance Existing Controls
Add a Label to all UI controls and render that Label similar to the current CheckBox. Adding some cool properties to control placement would also be in order. This would be a simplest solution by far. Very easy to style and auto layout with this solution. This solution also leverages the current layout panels, like the StackPanel enabling super fast form layout and very easy maintenance. Code generation would be simpler and the XAML much cleaner than today’s LOB form XAML.
When I was doing ASP.NET 1.1 development, I actually subclassed every UI control that shipped in Visual Studio 2003 and added features I needed like auto layout and two-way data binding. Another was the ability for each UI control to spit out a Label in the exact position I wanted it. This was accomplished with CSS and it was super easy laying out ASP.NET forms without any HTML Tables, etc. Each control, just knew how to lay itself out and render a Label.
Second Choice – New Form Control
I have not yet provided a Visual Studio 2008 design time experience for these controls but will in the next month or so. I will add drag and drop from the Toolbox and drag and drop reordering of controls within the Form using the WPF Designer. After I get some feedback, I’ll also add support for this control to XAML Power Toys. For now I’ve got to finish Ocean and prepare for two Code Camps in November.
Form Control Solution Overview
The simple solution presented here consists of three UI controls; Form, FormHeader and FormItem. These utilize WPF attached properties to control layout of child UI controls and the rendering of a Label for each child control.
The Form control subclasses ItemsControl. This allows me to take advantage of the built in ItemContainerGenerator to automatically wrap each child control in a FormItem control. This is very similar to a ListBox control that automatically wraps all child controls in a ListBoxItem control. The FormItem control provides the magic that adds the Label to the child control. The purpose of the FormHeader control is to provide a general purpose container that will not automatically be wrapped in a FormItem control.
Form Control
Public Class Form
Inherits ItemsControl
Shared Sub New()
DefaultStyleKeyProperty.OverrideMetadata(GetType(Form), _
New FrameworkPropertyMetadata(GetType(Form)))
IsTabStopProperty.OverrideMetadata(GetType(Form), New FrameworkPropertyMetadata(False))
End Sub
Protected Overrides Function IsItemItsOwnContainerOverride(ByVal item As Object) As Boolean
Return TypeOf item Is FormItem OrElse TypeOf item Is FormHeader OrElse TypeOf item Is Form
End Function
Protected Overrides Function GetContainerForItemOverride() As System.Windows.DependencyObject
Return New FormItem
End Function
End Class
The above code handles the automatic wrapping of the Form child controls in a FormItem. The IsItemItsOwnContainerOverride method provides the developer the opportunity to make a decision to wrap the child control or not. It would not make sense to wrap another Form, FormItem or FormHeader so we don’t.
The GetContainerForItemOverride method when overridden provides the hook to return a special container class for the object being created. This is the same method that many of you have overridden when customizing or extending the Menu or ListBox controls.
Also very worthy of mention is the IsTabStopProperty.OverrideMetadata in the shared constructor. This sets the IsTabStop property on the Form to false so that the developer won’t have to. All three of the controls presented here use this programming technique.
FormHeader
This is a simple class that derives from ContentControl. As mentioned above, it does not get wrapped in an FormItem control. It exposes a Content property that allows the developer to use this any way they choose. This control is not required, but I decided to include it in case developers wanted to section off their forms. (See the below image of a long form that has 4 of these)
FormItem
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pt="clr-namespace:XAMLPowerToys.Controls">
<Style TargetType="{x:Type pt:FormItem}">
<Setter Property="Padding" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type pt:FormItem}">
<DockPanel Margin="{TemplateBinding Padding}">
<Label x:Name="PART_Label"
Padding="0"
HorizontalAlignment="Left"
HorizontalContentAlignment="{Binding
Path=(pt:FormItem.LabelHorizontalContentAlignment)}"
VerticalAlignment="{Binding Path=(pt:FormItem.LabelVerticalAlignment)}"
Margin="{Binding Path=(pt:FormItem.LabelPadding)}"
DockPanel.Dock="{Binding Path=(pt:FormItem.LabelPosition)}"
Width="{Binding Path=(pt:FormItem.LabelWidth)}"
Target="{Binding}"
Content="{Binding Path=(pt:FormItem.LabelContent)}"
ContentTemplate="{Binding Path=(pt:FormItem.LabelContentTemplate)}"
ContentTemplateSelector="{Binding
Path=(pt:FormItem.LabelContentTemplateSelector)}" />
<ContentPresenter x:Name="PART_Control"
HorizontalAlignment="{Binding Path=(pt:FormItem.ContentHorizontalAlignment)}"
Margin="{Binding Path=(pt:FormItem.ContentPadding)}"
Content="{TemplateBinding Content}" />
</DockPanel>
<ControlTemplate.Triggers>
<Trigger SourceName="PART_Label" Property="pt:FormItem.LabelPosition" Value="Bottom">
<Setter TargetName="PART_Control" Property="DockPanel.Dock" Value="Top" />
</Trigger>
<Trigger SourceName="PART_Label" Property="pt:FormItem.LabelPosition" Value="Top">
<Setter TargetName="PART_Control" Property="DockPanel.Dock" Value="Bottom" />
</Trigger>
<Trigger SourceName="PART_Label" Property="pt:FormItem.LabelPosition" Value="Right">
<Setter TargetName="PART_Control" Property="DockPanel.Dock" Value="Right" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
#Region "LabelHorizontalContentAlignment"
Public Shared ReadOnly LabelHorizontalContentAlignmentProperty As DependencyProperty = _
DependencyProperty.RegisterAttached("LabelHorizontalContentAlignment", _
GetType(HorizontalAlignment), _
GetType(FormItem), New FrameworkPropertyMetadata(HorizontalAlignment.Left, _
FrameworkPropertyMetadataOptions.Inherits))
<Category("Layout")> _
<AttachedPropertyBrowsableForType(GetType(Control))> _
Public Shared Function GetLabelHorizontalContentAlignment( _
ByVal d As DependencyObject) As HorizontalAlignment
Return CType(d.GetValue(LabelHorizontalContentAlignmentProperty), HorizontalAlignment)
End Function
Public Shared Sub SetLabelHorizontalContentAlignment(ByVal d As DependencyObject, _
ByVal value As HorizontalAlignment)
d.SetValue(LabelHorizontalContentAlignmentProperty, value)
End Sub
#End Region
The FormItem derives from ContentControl. It exposes the required attached properties to allow styling of the child controls for the purpose of automatic layout and rendering of the UI control and its corresponding Label.
The FormItem ControlTemplate is straightforward. The DockPanel makes rendering of the Label on Top, Right, Bottom or Left of the control as easy as setting a property. Changes to the LabelPosition attached property cause one of the ControlTemplate Triggers to fire and move the Label as directed.
When moving the Label around, the developer may also want to set the LabelHorizontalContentAlignment attached property and possibly the LabelVerticalAlignment attached property. After investigating the control and its properties for 5 minutes you’ll figure out all the tricks this control can do and how it simplifies form layout.
I have not included all the attached property code as it’s pretty much a repeat of the above. There are two noteworthy pieces of code in the above snippet. First notice the FrameworkPropertyMetadataOptions. I have this set to Inherits. Most of the attached properties are written like this. This is the setting that allows this attached property to get its value from another control up the Element Tree. This is the main reason this solution is so clean. I can set the required attached properties on the parent Form control and not have to repeat them on each individual control. Heck, if I had multiple Form controls hosted in a Window or UserControl, I could set all the attached properties on the Window or UserControl if I wanted to.
Another possibility for setting of the attached properties is to put the settings in a style and apply it as you will see below.
The other noteworthy code is the System.Windows.AttachedPropertyBrowsableForType attribute. When applied to an attached property and the WPF Designer selected control matches the control type assigned in the attribute, the WPF Designer Property Grid will display the attached property. You should also apply the System.ComponentModel.Category attribute so that your attached property will displayed in desired category when the Property Grid is in category view as opposed to alpha view.
Styling Form & FormItem
<Window.Resources>
<Style x:Key="standardForm" TargetType="{x:Type pt:Form}">
<Setter Property="Margin" Value="11" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Gray" />
<Setter Property="Padding" Value="7" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Background" Value="WhiteSmoke" />
<Setter Property="pt:FormItem.ContentHorizontalAlignment" Value="Stretch" />
<Setter Property="pt:FormItem.LabelWidth" Value="100" />
<Setter Property="pt:FormItem.LabelPosition" Value="Left" />
<Setter Property="pt:FormItem.ContentPadding" Value="0" />
<Setter Property="pt:FormItem.LabelHorizontalContentAlignment" Value="Left" />
<Setter Property="pt:FormItem.LabelPadding" Value="0,0,0,0" />
</Style>
<Style BasedOn="{StaticResource standardForm}" x:Key="standardFormRightAlignedLabel" TargetType="{x:Type pt:Form}">
<Setter Property="pt:FormItem.LabelWidth" Value="75" />
<Setter Property="pt:FormItem.LabelHorizontalContentAlignment" Value="Right" />
<Setter Property="pt:FormItem.LabelPadding" Value="0,0,7,0" />
</Style>
<Style x:Key="bottomForm" TargetType="{x:Type pt:Form}">
<Setter Property="Margin" Value="11" />
<Setter Property="BorderThickness" Value="3" />
<Setter Property="BorderBrush" Value="Blue" />
<Setter Property="Padding" Value="0" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Background" Value="LightBlue" />
<Setter Property="pt:FormItem.ContentHorizontalAlignment" Value="Stretch" />
<Setter Property="pt:FormItem.LabelWidth" Value="100" />
<Setter Property="pt:FormItem.LabelPosition" Value="Bottom" />
<Setter Property="pt:FormItem.ContentPadding" Value="0" />
<Setter Property="pt:FormItem.LabelHorizontalContentAlignment" Value="Left" />
<Setter Property="pt:FormItem.LabelPadding" Value="0,0,0,0" />
<Setter Property="pt:FormItem.LabelVerticalAlignment" Value="Top" />
</Style>
<Style TargetType="{x:Type pt:FormItem}">
<Setter Property="Margin" Value="7,7,0,0" />
</Style>
</Window.Resources>
Finally we can see some XAML layout markup. I like that it’s centralized and simple. Just by tweaking a few properties I can change the rendering of my forms. In a production application these styles would live in the various application skin resource dictionaries or lacking skins, in application level resource dictionaries. I also like that I can alter my form layouts without having to dig around in the XAML markup.
Example: want more spacing between rows of controls, no problem, just look at the last style. A single line of markup and I’m done.
You can also see I’ve taken advantage of the BasedOn property of the second Style that allows the second style to inherit the properties of the first style and selectively modify properties. Those of you who are familar to CSS will appreciate this super feature of WPF.
Form Control Features
By design, the above form does not have the separation between rows of controls like the other forms. Also this form while not designer approved, shows off some goodness that is under the hood.
First go ahead and run the included sample application. Tab though the form. Notice how it just works.
Now press the <ALT> key. This will cause the Access Key for each field to display. Yes, I know that I have used A, C & Z twice. I did this to illustrate different ways to wire up the Access Key. My thanks again to Dr. WPF for showing this to me.
Below is the XAML markup for each of the addresses without the FormHeader control.
<!-- Home Address -->
<TextBox pt:FormItem.LabelContent="_Address" />
<StackPanel Orientation="Horizontal" pt:FormItem.LabelContent="City State Zip">
<TextBox Width="150" />
<TextBox Margin="7,0,7,0" Width="35" />
<TextBox Width="75" />
</StackPanel>
<!-- Work Address -->
<TextBox pt:FormItem.LabelContent="_Address" />
<StackPanel Orientation="Horizontal">
<pt:FormItem.LabelContent>
<StackPanel Orientation="Horizontal">
<Label Target="{Binding ElementName=txtCity}" Padding="0">_City</Label>
<Label Target="{Binding ElementName=txtState}" Padding="3,0,3,0">_State</Label>
<Label Target="{Binding ElementName=txtZip}" Padding="0">_Zip</Label>
</StackPanel>
</pt:FormItem.LabelContent>
<TextBox x:Name="txtCity" Width="150"/>
<TextBox x:Name="txtState" Margin="7,0,7,0" Width="35" />
<TextBox x:Name="txtZip" Width="75" />
</StackPanel>
<!-- Vacation Address -->
<TextBox pt:FormItem.LabelContent="_Address" />
<pt:Form Margin="15,0,0,0">
<pt:Form.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</pt:Form.ItemsPanel>
<TextBox pt:FormItem.LabelContent="_City" pt:FormItem.LabelWidth="100" Width="130" />
<TextBox pt:FormItem.LabelContent="_State" Width="35" />
<TextBox pt:FormItem.LabelContent="_Zip" Width="65" />
</pt:Form>
The home address does not wire up the Access Keys. It demonstrates one way to utilize a StackPanel to render controls horizontally.
The work address shows how to customize the LabelContent property of a child control (in this case a StackPanel) and insert a StackPanel that contains Labels that are associated with TextBoxes. These Labels have their Access Keys set.
The vacation address is displayed in a nested Form control that has its ItemsPanel set to a StackPanel with Horizontal Orientation.
Downloads
After downloading the below package, you must rename it from .doc to .zip. This is a requirement of WordPress.com
Download Source 52KB
Close
I think this solution, a similar or better one, will help all of us when developing and maintaining our WPF & Silverlight LOB applications. In addition developers transitioning from Win Forms will find a friendly and very productive environment awaiting them.
Please leave both positive and negative feedback on the two solutions proposed here. Do you have another solution? Please blog it and send me the link.
Have a great day!
Just a grain of sand on the worlds beaches.