VSM Cover Story

WPF Goes to Work

Windows Presentation Foundation has been greatly underserved by Microsoft’s emphasis on glitz and glamour; learn how to take advantage of WPF in your everyday Windows business apps.

Technology Toolbox: VB.NET, C#, XAML

It might surprise some people at Microsoft, but I've never been asked to build an application that put photographs on spinning cubes. Nor have I ever been asked to include video in my apps. I suspect few developers have.

Yet that is precisely what product managers and developers at Microsoft have been showing off when discussing the developer-related features of Windows Presentation Foundation (WPF). This is unfortunate because Microsoft's focus on the glitz and glamour features of WPF obscures how great the technology is for writing business applications, even with the current crop of imperfect design tools.

I'm excited about WPF's better code design compared to Windows Forms; I'm impressed by the fact that well architected applications written in WPF can have an artistic or outrageous appearance with little developer effort.

I'll show you how to put these practical, business-oriented features to use in a real-word scenario. This article's sample is an n-tier business application with a couple of related business objects and a lookup table. The business objects derive from a common base that is similar to many other business object libraries that exist today. Rather than toss out this good architecture and business logic, I'll show you how to leverage your architecture in WPF with a design that further isolates layout and data plumbing from behavior and appearance.

The magic behind WPF is all there if you look for it: content usage, routed events, styles, templates, and conversions in the XAML loader; for more on XAML basics, please see my article, "XAML: Rethink How You Code UIs" [VSM June 2007]. XAML is nothing more than a declarative way to create objects so you can do the same things in Visual Basic or C# code. XAML elements are .NET classes, usually controls. XAML attributes are .NET properties, so don't be confused if I use a different term than you're expecting.

The data binding model in WPF lets you bind any properties to many different things, including resources, files, objects, and properties. While Windows Forms and WebForms allow fairly flexible binding, you nearly always use them to tie business data to a small handful of properties such as Text and SelectedValue. WPF stretches binding to include styles, templates, tooltips, brushes, and other features. WPF is highly flexible, but its binding model includes several important defaults and fallback patterns that minimize the code you need to write.

A well organized WPF application separates content--such as things data and labels use -- from appearance. Templates are part of the styling model, and they extend what styles can do. Data templates control display of repetitive items, such as showing a consistent set of data in list and combo boxes. Perhaps you want only a standard combo box now, but at some future point you might want to add an icon, tooltip, or information block such as an address. If you've abstracted your data templates, you can apply this change in a single location. If you use the enum wrapper., you can apply this change once for all of your enums. (See the article "Use Enums Across Assembly Boundaries" by Kathleen Dollard for more information on enums [Q&A, VSM May 2007].) Control templates let you redefine the visual display of common elements, even allowing you to make big changes such as converting all round buttons into circles in a single location.

Build Out Your UI
WPF lets you build forms (called Windows in WPF parlance) and user controls. User controls are a good choice for entities in business applications because they can be contained in parent windows that that can morph over time. The user control can define as little as the data layout (the grid) and perhaps a data template for any lookup controls such as a combo box (see Listing 1 and Figure 1). This simple layout adjusts for changing caption lengths and font sizes, as well as for explicit changes in appearance through styles defined outside the user control.

Figure 1
[Click on image for larger view.]
Figure 1. Design Business UIs Quickly and Effectively.
WPF lets you design business user interfaces that range from fancy to conservative in appearance. Sophisticated features like decorators let you build in authorization based on logic in your business objects, such as hiding the gender field from some users. Decorators and styles can also leverage existing business logic with automatic error indicators and error message tooltips.

Most controls in WPF are "content" controls that can contain only one item. This item can be either content such as a TextBlock, or it can be one of the layout controls. The most important layout controls for business applications are StackPanels and Grids. Grids, which are similar to HTML tables or WinForms TableLayoutPanels, automate layout, without using absolute positions for controls. You should avoid absolute positions when designing UIs because they respond poorly to font changes and localization. You define grids by creating an element for each row or column. Supplying Auto for the Width or Height sizes the row or column to its tallest or widest content.

Labels are content controls. Like all other WPF content controls, labels contain a single element. This element can be complex, which opens up some exciting business-case possibilities. Adding an image that indicates required fields requires only adding a container control (a StackPanel control, in this case) and the new image:

<Label Grid.Row="0" Grid.Column="0" 
   Target="{Binding ElementName=FirstNameTextBox}" >
   <StackPanel Orientation="Horizontal">
      <AccessText>_First Name</AccessText>
      <Image Source="Images\Required.png"/>
   </StackPanel>
</Label>

Labels include a target property that specifies the control to jump to when the user hits the access key. You define access keys in AccessText objects, which display with an underscore that specifies the access key when the user hits the Alt key.

The Target attribute of the Label control is a binding that uses the ElementName named parameter to specify another control. The curly bracket syntax indicates a markup extension that literally calls the constructor on the specified class. Items can be bound to many things, including other elements within the current context. This explicit linking of labels and their associated controls is far easier to use than the TabIndex approach of Windows Forms. Referencing a control through the ElementName syntax requires that the target control be named. Controls that are not referenced in ElementName bindings or in .NET code do not require names. Typically, you name editing controls so labels can use them as targets; you don't name labels and container controls.

You use the binding markup extension to bind controls to the data the user is editing. Controls bind to data using the Source and Path binding parameters:

<TextBox Name="LastNameTextBox" 
   Margin="3" Grid.Row="2" 
Grid.Column="1" Text="{Binding 
   Path=LastName}"/>

If the Source isn't specified, it defaults to the DataContext of the container, or the nearest ancestor with a non-null DataContext. The Path specifies the specific property for binding, similar to the Window Forms' DataMember property.

Drill Down on List and Combo Boxes
List and combo boxes are a bit more complicated because you need to bind both the value from the current data record and the list of options that are available. In almost all cases, you want the combo box for a specific list (such as a list of genders) to be the same wherever it appears in your application. You define this appearance using data templates. Data templates are nothing more than directions that describe how to display the lookup records, and you can use the same data templates in list and combo boxes.

Defining a data template inline inhibits reuse and makes it harder to achieve a consistent appearance. Instead, you should name your templates and place them at the document root (user control or window) or as a global resource in the App.xaml file. Defining the template in the App.xaml file lets you define the template once and use it everywhere. Even if you're using simple list items now, this abstraction allows quick integration of more complex data in the future. You access resources such as styles and data templates using their x:Key identifier, which is an attribute on the template or other resources.

You access a data source in WPF with an ICollectionView that provides the current position, sorting and filtering, and other capabilities. You can access the default ICollectionView using the GetDefaultView shared method of the CollectionViewSource. By default, WPF doesn't synchronize the data source and the list or combo boxes the data source is bound to. You can force synchronization using the IsSynchronizedWithCurrentItem attribute.

In addition to providing the XAML definition, you need to tell the business object to retrieve data. You can do this in XAML, but it's simpler to keep XAML for declarations and drop the load process into VB or C# code.

Partial Public Class PersonUserControl
Inherits System.Windows.Controls.UserControl
   Public Sub New()
   InitializeComponent()
      Dim genderDisplay As _
         BizObjects.EnumDisplayCollection
      genderDisplay = _
         New BizObjects.EnumDisplayCollection( _
         GetType(BizObjects.Gender))
      GenderComboBox.ItemsSource = genderDisplay
   End Sub

   Public Sub Load(ByVal id As Int32)
      Dim person As BizObjects.Person = _
         BizObjects.Person.GetItem(id)
      Me.DataContext = person
   End Sub

End Class

This is all that's required to create the data layout in XAML. Everything else happens in styles, triggers, and templates. The layout is isolated strictly from the appearance and behavior.

Before diving into how you manipulate the appearance and behavior of your WPF UIs, try dropping a user control onto a window. The window uses click events and commands to manage menu items (see Listing 2). The XAML specifies the event handler, and you include the behavior you want in the Click event of your VB or C# code behind file:

Private Sub CustomerClick(ByVal sender _
   As Object, ByVal e As RoutedEventArgs)
   Dim form As New Customer()
   form.Show()
End Sub

Commands let you isolate initiation (usually a button or menu item) from the action. It also offers a mechanism for enabling and disabling all initiating elements. You can add a button that does the same thing as a menu item by defining the same command on both UI elements. WPF commands provide a routing mechanism that doesn't perform any action on its own. You declare the action in .NET code that runs when the command executes and, optionally, when the command is enabled (the CanExecute method) through command bindings.

.NET ships with a large number of commands. A handful of these have command bindings wired up for common scenarios such as Cut, Copy, and Paste from text boxes. You add your own commands by creating a RoutedUICommand and supply your own behavior for standard and custom commands in .NET code:

Private Sub CloseCommandExecuted(ByVal sender _
   As Object, ByVal e As ExecutedRoutedEventArgs)
   Me.Close()
End Sub

Private Sub CloseCommandCanExecute( _
   ByVal sender As Object, ByVal e As _
   CanExecuteRoutedEventArgs)
   ' Always display Close initiating controls 
   e.CanExecute = True
End Sub

Wire Up Commands
You wire the behavior together using CommandBindings in the XAML file:

<CommandBinding 
   Command="ApplicationCommands.Close"
   Executed="CloseCommandExecuted"
   CanExecute="CloseCommandCanExecute"/>

The UI now works, but there's no validation and all fields are displayed for all users, when only certain users should see the birth date according to business rules.

WPF offers three approaches to validation. The most obvious is to specify validation rules in the XAML page. Validation rules are .NET code in classes that derive from ValidationRule and override the Validate method. The Validate method returns a Validation-Result object that indicates whether validation was successful. This approach might be easy and well described in the documentation, but it's undesirable because validation represents business logic and moving validation into the UI breaks encapsulation.

WPF delivers a single ValidationRule that responds to exceptions. This lets you bind to business properties that throw exceptions when bad data is passed. If your business objects already throw exceptions for bad data, this is a valid strategy. But there are good reasons, such as allowing cross field validation, that prompt many people elect to use the alternate strategy of allowing bad data to reside temporarily in business objects. You should not redesign your business objects to support your UI layer. You could use reflection to write some clever validation rules that generically validates your controls. But you'd have to link this validation to every control, a process that is not only boring, but prone to introducing bugs.

Ideally, you want a simple mechanism that validates against your business objects automatically, alters the UI appearance to indicate data errors, and can be added to windows or user controls easily. You must write some code, but you can accomplish this and authorization using custom decorators. The ValidationDecorator and AuthorizationDecorator provide validation and authorization functionality tied to the existing business-object design. Your design might be different, so you might need to alter and expand the example. The article's base business object already supports the IDataErrorInfo interface because they were designed to work with ErrorProviders in Windows Forms. For authorization, the base business object includes overridable CanRead and CanWrite methods that take a property name argument and return a Boolean.

Decorators wrap a block of XAML that acts as a container and provides extra functionality. You can use decorators for visual tricks such as borders, or for more complex tasks such as the validation and authorization functionality. Complex validators often require complex reflection to find the data bindings, so you might want to create a DecoratorBase to do the heavy lifting. I recommend creating and cacheing the bindings the first time you need them. Also, it makes sense to offer two overloads of a ForAll method. I recommend a ForAll instead of a ForEach method because it's a deep traverse through the entire hierarchy within the decorator. The code is a little long and messy, so I've included it only in the download.

Decorators Simplify Coding
Labels are connected to editable controls explicitly, so they should have the same authorization behavior and should disappear when the associated control disappears. To accomplish this, the GetAssociatedLabel method searches all the elements contained in the decorator looking for labels targeting the element of the binding and caches this as well.

Code in the ValidationDecorator automatically hooks up the LostFocus event of each contained control to the Validate method of the decorator. The Validate method of the ValidationDecorator calls the ForAll method to loop through the bindings, passing the ValidateElement method as a delegate. The ValidateElement method takes advantage of its base class functionality and the IDataErrorInfo interface implemented by the business objects:

Protected Sub ValidateElement( _
   ByVal element AsFrameworkElement, _
   ByVal binding As Binding, _
   ByVal prop As DependencyProperty, _
   ByVal associatedLabel As Label)
Dim expression As BindingExpression = _
   element.GetBindingExpression(prop)
   Dim dataErrorInfo As IDataErrorInfo = _
      TryCast(Me.BoundObject, IDataErrorInfo)
   If expression IsNot Nothing _
      AndAlso dataErrorInfo IsNot Nothing Then
Dim message As String = dataErrorInfo(binding.Path.Path)
   If String.IsNullOrEmpty(message) Then
      Validation.ClearInvalid(expression)
   Else
Dim validationError As ValidationError = _
   New ValidationError( _
   New ExceptionValidationRule(), _
   expression, message, Nothing)
      Validation.MarkInvalid(expression, validationError)
      Me.mIsValid = False
      End If
   End If
End Sub

After finding the business object, the ValidateElement method casts the business object to IDataErrorInfo and calls the default method, which returns either Nothing or a string describing the validation error. WPF offers the ValidationError class, which marks the control as invalid.

Finding the business object isn't trivial because it might be bound as a collection or item, either directly or through a Windows.DataSourceProvider. Sorting this out is isolated in the BoundObject property:

Protected ReadOnly Property BoundObject() AsBizObject
   Get
   Dim dataSource As Object = Nothing
   Dim dataSourceProvider As DataSourceProvider = _
      TryCast(Me.DataContext, DataSourceProvider)
   If dataSourceProvider IsNot Nothing Then
      dataSource = dataSourceProvider.Data
   Else
      dataSource = Me.DataContext
      End If
   Dim bizObject As BizObject = TryCast(dataSource,BizObject)
      If bizObject Is Nothing Then
      Dim collection As ICollectionView = _
         CollectionViewSource.GetDefaultView( _
         Me.DataContext)
         bizObject = TryCast( _
            collection.CurrentItem,BizObject)
      End If
      Return bizObject
   End Get
End Property

Authorization isn't supported by a built-in .NET interface, so the base class includes CanRead(propertyName) and CanWrite(propertyName) methods. The AuthorizationDecorator is similar to the ValidationDecorator, except it doesn't hook into specific UI events. Instead, it sets up the window or user control when it loads. The AuthorizationDecorator sets the Visibility property of both the bound control and its associated label to Collapsed, which hides the control and reclaims the space.

The appearance of the user interface is sometimes belittled as eye-candy, but it serves important purposes. Without an artist, it's best to stay with relatively conservative user interfaces. However, the appearance of these interfaces conveys important information. A UI that just disabled the Save button but neglected to indicate which fields were invalid would be extremely frustrating. User interface appearance can indicate the validation status, more rapidly cue users to selections (such as sketches of building types), and convey key information about the business data. Implementing these facets of your UI will make your application more appealing to use.

You don't have to always supply property values like colors and fonts because the values fallback up the tree using a mechanism called ambient properties. Due to ambient properties, most values that you don't provide default to the containers value for the property. You should use resources and styles to isolate the specific values when you want to apply a specific appearance.

Hook Up Resources and Styles
Bindings can link to static or dynamic resources within your application. Resources can include images, colors, templates, styles, or any other WPF or .NET type. XAML finds resources by searching up the tree to find the first match. You can provide a resource at many different layers within the user control itself. But providing resources within user controls scatters key information and makes later enhancements more difficult. You should prohibit any visual touches within entity windows and user controls. Instead, abstract resources, styles, and templates into a global location such as your App.xaml file.

Resources are useful for specific items like colors and fonts. They can also prove invaluable in defining other styles to improve reuse and readability. You can even define resources in terms of other resources:

<Color x:Key ="ErrorStartColor">White</Color>
<Color x:Key ="ErrorEndColor">Red</Color>

<LinearGradientBrush x:Key="ErrorGradientBrush" 
   StartPoint="0,0" EndPoint="1,1">
   <GradientStop Offset="0" 
      Color="{StaticResource ErrorStartColor}"/>
   <GradientStop Offset="1" 
      Color="{StaticResource ErrorEndColor}"/>
</LinearGradientBrush>

Styles let you define default property values either by control type or specific names and can alter one or many visual features:

<Style TargetType="{x:Type ComboBox}" 
   x:Key="SpecialComboBox">
   <Setter Property="Margin">
      <Setter.Value>
         <Thickness Left="3" Top="3" Right="10" Bottom="3"/>
      </Setter.Value>
   </Setter>
</Style>

This code uses a named style, so XAML will use it only for controls that define this style. More often, you'll have a style intended for all controls of a given type. You can still override this value at the local level, but you'll want to avoid that approach because it discourages reuse. Instead, you should access a global style that describes why the control is different. For example, create a style named "SelectionControl" if you want all your selection controls to look different than data entry controls, or if you want to leave the door open to employing a different appearance in the future.

Styles can have triggers that apply an appearance when the specified property matches a defined value, and removes the trigger when it no longer matches. This lets you provide a custom visual appearance to all invalid textboxes with a style similar to this one:

<Style TargetType="{x:Type TextBox}">
   <Style.Triggers>
      <Trigger Property="Validation.HasError" Value="True">
         <Setter Property="Background" 
         Value="{StaticResource ErrorGradientBrush}"/>
         <Setter Property="ToolTip"
      Value="{Binding RelativeSource={RelativeSource Self},
            Path=(Validation.Errors)[0].ErrorContent}"/>
      </Trigger>
   </Style.Triggers>
</Style>

The textbox has a property that returns a validation object with a HasError property. This property is set in the MarkInvalid method called in the ValidateElement method of the decorator discussed earlier. When HasError is true, WPF sets the background color to the red gradient automatically. It also binds the tooltip to the Validation.Errors property of the current control. Including code like this in the App.xaml file offers visual behavior on all textboxes that don't have a style specified.

Create Special Styles
You might want to create special styles for some circumstances and still reuse the validation visuals or other style values. To do this, build your new style using the BasedOn attribute of the style. In the article code sample, I give a standard margin to combo boxes through a style based only on the type. Another style provides a data template specifically for enum combo boxes. Rather than try to maintain a redundant copy of the margin settings, the BasedOn attribute refers to the original style.

You can also describe combo and list box data displays in the App.xaml file. For example, take another look at Figure 1, which displays a list box that allows record selection and a combo box that allows gender selection. The gender combo box displays an enum based on the EnumDisplay and EnumDisplayCollection classes. EnumDisplay assumes a resource is available for the description along with an image. Based on these assumptions, the style sets the ItemTemplate used to display each value in the ComboBox. The ItemTemplate property holds a DataTemplate. The DataTemplate defines nested horizontal and vertical StackPanels for layout:

<Style x:Key="EnumCombo" TargetType=
   "{x:Type ComboBox}"
      BasedOn="{StaticResource {x:Type ComboBox}}">
   <Setter Property ="IsEditable" Value="True"/>
   <Setter Property ="SelectedValuePath" Value="Value"/>
   <Setter Property ="ItemTemplate">
      <Setter.Value>
         <DataTemplate >
         <StackPanel Orientation="Horizontal">
         <Image Source=
            "{Binding Path=ImageSource}"/>
         <StackPanel Orientation=
            "Vertical" 
            HorizontalAlignment="Stretch">
         <TextBlock Text="{Binding Path=Text}" 
            Background="LightGreen" 
            FontSize="16" FontWeight="Bold"/>
         <TextBlock Text=
            "{Binding Path=Description}"/>
         </StackPanel>
         </StackPanel>
      </DataTemplate>
   </Setter.Value>
   </Setter>
</Style>

The Image Source property binding illustrates the flexibility of the type converters. A string isn't an image (obviously), but when faced with a string, the converter attempts to find and load a file with that name. All enums in your application can use this style, and you can vary it in one place to suit your application.

Binding the selection combo box is a little different. You want the same data wherever you select an item such as a customer or incident. However, every business object is described differently. In this case, it's easiest to encourage reuse by building a selection user control. The data template for combo box items is used only within this user control, so describe it as a user control resource.

The selection combo box should display the alternate keys that identify the specific record for the user. It can also display additional information, such as which items are late, through visual cues (see Figure 2). Visual cues are easy to accomplish using data triggers (see Listing 3). WPF uses the base data template unless the incident is in a special state. WPF recognizes when the data moves in or out of this state and applies the special features automatically. Once the resources and triggers are in place, the data template is nearly trivial to write; all you need to do is display the incident title as a string. Using the IncidentSelection user control requires only one line of code in the Incident user control:

Figure 2
[Click on image for larger view.]
Figure 2. Highlight Important Conditions
The appearance of individual data items can indicate important business conditions, such as overdue items or items assigned to the current user. Creating a selection user control encourages reuse of the data selection and enables you to provide a consistent user interface for your users.
<biz:IncidentSelection Grid.ColumnSpan="2"/>

The downside of working with WPF is that declarative models don't lend themselves to supplying compiler errors. Many of the mistakes you make will sneak past the compiler and appear at runtime when the window or user control is loaded; in other cases they will simply be ignored. The 3.5 designer and Expression Blend tool provide better error messages than the 3.0 designer, but you'll spend significant time tracking down why WPF isn't behaving as expected.

It is only a matter of time until developers at Microsoft provide WPF with high quality designers. But you don't need to wait to take advantage of WPF in your applications today. The built in command model, composite user controls, templated list and combo boxes, powerful decorator model, extensive styling, and many other features make WPF compelling today. The edges are rough, but smooth them down and you find a great way to create applications.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube