Sunday, August 3, 2008

Custom Markup Extensions in WPF

Since I have been working with Windows Presentation Foundation, I have been looking for a good reason to make my own custom markup extension. What follows is the problem that I wanted to solve, a brief overview of markup extensions and how I implemented my solution.
Problem to be solved
Every ListBox and ComboBox needs a list of options and nothing is more irritating to me than seeing a long list of options directly hard-coded into an application. Therefore, pretty much every project that I have worked on, there has been a need to create a database table to manage selection options.
Typically such a table has been named ListOptions, ControlData, Keywords or something similar and has had columns for the key, display text, value, and display order. So for example, to populate a Comb Box of name prefixes, such as Mr., Mrs., Dr., etc, a simple query to filter the key on NameSuffix is used to build the list.
As you can imagine this is only used for adhoc scenarios where the values do not have a supporting relational table and it is desirable to store a value versus a database key. Obviously if I wanted to store for example a sales person associated to an order, that would be a relational join to a SalesPerson table and the control would be bound to that table for a list of sales persons.
Here is example code that simulates the look-up table, which will be referenced later. Of course the real code would create the ListOption objects from data in the database table.
'This example uses the Linq so be sure to reference and import System.Data.Linq

Module ListOptions
    'This example creates the data array in code.  In a real app this would be a
    'database query in place of this code.

    Private _options As ListOption() = { _
        New ListOption With {.Key = "ShirtColor", .Display = "Red"}, _
        New ListOption With {.Key = "ShirtColor", .Display = "Green"}, _
        New ListOption With {.Key = "ShirtColor", .Display = "Blue"}, _
        New ListOption With {.Key = "ShirtSize", .Display = "Small"}, _
        New ListOption With {.Key = "ShirtSize", .Display = "Medium"}, _
        New ListOption With {.Key = "ShirtSize", .Display = "Large"}, _
        New ListOption With {.Key = "NamePrefix", .Display = "Mr."}, _
        New ListOption With {.Key = "NamePrefix", .Display = "Mrs."}, _
        New ListOption With {.Key = "NamePrefix", .Display = "Ms."}, _
        New ListOption With {.Key = "NameSuffix", .Display = "Jr."}, _
        New ListOption With {.Key = "NameSuffix", .Display = "Sr."} _
        }

    Function GetOptions(ByVal key As String) As List(Of ListOption)
        'Do a simple LINQ expression to return all ListOption objects that match
        'the specified key
        Return (From lo In _options Where lo.Key = key Select lo).ToList
    End Function
End Module
'Class to represent ListOption Table Row
Public Class ListOption
    Private _key As String
    Public Property Key() As String
        Get
            Return _key
        End Get
        Set(ByVal value As String)
            _key = value
        End Set
    End Property

    Private _display As String
    Public Property Display() As String
        Get
            Return _display
        End Get
        Set(ByVal value As String)
            _display = value
        End Set
    End Property

    '...
    'Also would need value member, display order, etc.
End Class
Typical Solutions
In WPF there are two immediate solutions that might come to mind for most developers to bind the options to a control's ItemsSource property.
The first thought might be adding an ObjectDataProvider to call a method in your class that returns a list.
<window class="Window1" title="Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" width="300" local="clr-namespace:WpfApp1" height="300" x="http://schemas.microsoft.com/winfx/2006/xaml">
     <window.resources>
          <objectdataprovider methodname="GetListOptions" objecttype="{x:Type local:ListOptions}" key="ColorOptions">
               <objectdataprovider.methodparameters>
                    NamePrefix
               </objectdataprovider.methodparameters>
          </objectdataprovider>
     </window.resources>
     <grid>
          <listbox name="MyListBox" height="80" displaymemberpath="Display" itemssource="{Binding Source={StaticResource ColorOptions}}">
     </grid>
</window>
The other solution might be to set the DataContext in the load event.
Class Window1
     Private Sub Window1_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
          MyListBox.ItemsSource = GetListOptions("NamePrefix")
     End Sub
End Class
But what about a simpler and more extensible solution? What if you could just simply make a reference in the ItemsSource property to initiate and return the lookup? The solution: a custom Markup Extension.
Markup Extension Overview
Markup Extensions are a key part of WPF and when first learning WPF it may not even be obvious of what one is or the fact that you can create you own.
So what is a Markup Extensions? Here is a short except from the .Net Online help under the topic "XAML Overview"
Markup extensions are a XAML concept. In attribute syntax, curly braces ({ and }) indicate a markup extension usage. This usage directs the XAML processing to escape from the general treatment of attribute values as either a literal string or a directly string-convertible value.
Here are a few examples
{x:Static ...}
{x:Type ...}
{StaticResource ...}
The very first part of a Markup Extension expression after the opening brace references the class to call. You might be thinking there is no class called "Static" in the .Net Framework. And you are right there isn't. This is because Markup extensions are assumed to have the suffix of "Extension". So now that you know this you can go look in the online help and find StaticExtension, TypeExtension, and StaticResourceExtension, which are all perfectly defined.
So now that you know that the first keyword is a class reference, let's go create our very own "Hello World" Markup Extension called MyTestExtension.
Besides our class name needing to end with "Extension" there is one other important requirement to take note of. Our class will need to inherit from MarkupExtensionBase and override the ProvideValue function.
Public Class HelloWorldExtension
    Inherits Markup.MarkupExtension

    'This method is called by the XAML processor.
    Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
        Return "Hello World!"
    End Function
End Class
And the XAML code that uses a TextBox to display the results.
<Window x:Class="Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApp1"
    Title="Window1" Height="300" Width="300" Name="Window1">
     <Grid>
        <TextBox Text="{local:HelloWorld}" VerticalAlignment="Bottom" Height="20" />
    </Grid>
</Window>
Let's now take a look at the other part of a markup extension expression. Either the class's proprieties are set using the X=VALUE method, or values are simply listed in order of the class's New constructor.
For example, the StaticResource class can be called either of the following ways:
{StaticResource MyKey}
{StaticResource ResourceKey=MyKey}
The first line calls the New constructor with the value specified. The second line creates a new StaticResourceExtension object and then sets the ResourceKey property to the specified value. Either way should always result in the same return provided by the ProvideValue function.
Here is an example of our new MyTestExtension class, now a little more versitle by using a parameter.
Public Class HelloWorldExtension
    Inherits Markup.MarkupExtension


    Private _name As String
    Public Property Name() As String
        Get
            Return _name
        End Get
        Set(ByVal value As String)
            _name = value
        End Set
    End Property

    'Default Constructor
    Public Sub New()

    End Sub

    'Constructor to set name property
    Public Sub New(ByVal name As String)
        _name = name
    End Sub

    'This method is called by the XAML processor.
    Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
        Return "Hello World! From " & _name
    End Function
End Class
And the XAML code that uses a TextBox to display the results using either method.
<Window x:Class="Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApp1"
    Title="Window1" Height="300" Width="300" Name="Window1">
     <Grid>
        <TextBox Text="{local:HelloWorld John}" VerticalAlignment="Top" Height="20" />
        <TextBox Text="{local:HelloWorld Name=John}" VerticalAlignment="Bottom" Height="20" />
    </Grid>
</Window>
If you are doing this with the split Designer/Code view in Visual Studio 2008 you should be noticing that the results from the HelloWorldExtension class are displayed in the designer pane. Pretty cool, huh?
The solution
Now for the solution originally promised.
<Markup.MarkupExtensionReturnType(GetType(List(Of ListOption)))> _
Public Class ListOptsExtension
    Inherits Markup.MarkupExtension

    'Define our member and property for the Key
    Private _key As String
    Public Property Key() As String
        Get
            Return _key
        End Get
        Set(ByVal value As String)
            _key = value
        End Set
    End Property

    'Default Constructor
    Public Sub New()
    End Sub

    'Constructor with key specified as parameter
    Public Sub New(ByVal key As String)
        _key = key
    End Sub

    'This method is called by the XAML processor.
    Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
        Return GetListOptions(_key)
    End Function
End Class
And now the grand finale. Look how easy it is to set the ItemsSources property with options loaded from a table!
<window class="Window1" title="Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" name="Window1" width="300" local="clr-namespace:WpfApp1" height="300" x="http://schemas.microsoft.com/winfx/2006/xaml">
<grid>
<listbox name="MyListBox" height="80" displaymemberpath="Display" itemssource="{local:ListOpts NamePrefix}">
</grid>
</window>
Now with just a few keystrokes in our XAML code, we can easily fill every ComboBox or ListBox we have in our application. I love WPF and how extensible it is!
Extra Notes
Here is an important side note regarding ListBoxes and ComboBoxes. A typical application will probably be required to bind the selection of the ListBox or ComboBox control to a business object. In this example, if the SelectedValuePath property of the ListBox control was not specified, binding the SelectedValue property would reference the ListOption object instead of the string value that should be referenced. With the SelectedValuePath property specified, the binding between SelectedValue and a business object's string property will link up perfectly.
Taking Markup Extensions Further
I have also created markup extensions for places where I need a computed value, such as the currently logged in user. So Markup Extensions are also very comparable to macro capabilities found in other markup language languages.

No comments: