Tuesday, July 7, 2009

IMultiValueConverter with Silverlight

A converter is a powerful concept with WPF and Silverlight. It implements IValueConverter and allows you to map a value to a display entity. For example, if you have an application that stores temperatures, you might use a converter to map the temperature to the image of a thermometer that visually represents the temperature. A weather application might take the text "party cloudy" and map this to an image of clouds.

While Silverlight supports value converter, one limitation is that you cannot convert multiple values at once. For example, if you are converting a value in a data grid, your converter doesn't, by default, have access to the other values or properties in the row. This makes it difficult, for example, to take a first name and last name and convert those into a single representation of "full name."

There is a workaround! With a little creativity you can do just about anything.

In my case, I had a data grid and wanted to show a hyperlink button. I needed two values to go into the button: the text to describe the link, and an internal id to pass to the link. Consider a product list: my products have internal identifiers and external descriptors. So identifier with ID=1 maps to product with Name="Backpack". How do I create a link like this:

<a href="http://localhost/products.aspx?id=1">Backpack</a>

The first step is to create a control that can accept multiple bindings. We do this by adding a dependency property so data can be bound to the property. In this case, I simply inherited from the HyperlinkButton and added my identifier property:

public class ProductHyperlink : HyperlinkButton
{
    private const string BASEURI = "http://localhost/products.aspx?id={0}";

    public static readonly DependencyProperty IDProperty =
        DependencyProperty.Register("ID", typeof (int), typeof (ProductHyperlink), null);

    public ProductHyperlink()
    {
        Click += _ProductHyperlinkClick;
    }

    void _ProductHyperlinkClick(object sender, RoutedEventArgs e)
    {
        NavigateUri = new Uri(string.Format(BASEURI, ID));            
    }

    public int ID
    {
        get
        {
            return (int) GetValue(IDProperty);
        }
        set
        {
            SetValue(IDProperty, value);                 
        }
    }
}

Notice how I intercept the click and bind the id with the URL to my Uri. This enables me to navigate directly to the link I want. The data grid looks like this:

<data:DataGrid x:Name="DeviceGrid" AutoGenerateColumns="False" Margin="5">        
    <data:DataGrid.Resources>            
        <DataTemplate x:Key="ProductTemplate">
            <Controls:ProductHyperlink ID="{Binding ID}" Content="{Binding Name}"></Controls:DeviceHyperlink> 
        </DataTemplate>
    </data:DataGrid.Resources>
    <data:DataGrid.Columns>
        <data:DataGridTemplateColumn data:Header="Product" CellTemplate="{StaticResource ProductTemplate}" CanUserSort="True" 
                 SortMemberPath="Name"/>                
    </data:DataGrid.Columns>
</data:DataGrid> 

We create a template that binds both the ID and the Name to our custom control. Remember, the control will turn the ID into a Uri that links to the product when it is clicked by the user. The column simply maps to the template. Note that by indicating the user can sort and providing a sort member path, I instruct the grid automatically how to sort my custom column even though I'm not binding directly to a single property.

That's all there is to it - you can use a similar method by creating custom panels, text blocks, etc, and even taking multiple bindings and injecting the custom controls inside your custom container.

Jeremy Likness