Thursday, February 26, 2009

Design time sample data in XAML

Over the last couple weeks I have been struggling with the issue of how to provide sample data for our designers to use in Blend while doing layout and having that data not created during run time. Even more importantly, the sample data should provide a way for designers to specify bindings through the UI without having to manipulate the XAML code directly

The goal is to provide the following:
  1. A base class that allows developers to create simple derived classes that specify sample data
  2. Able to set the DataContext property in Expression Blend by clicking the New button and selecting the appropriate sample data class from the tree view.
  3. Be able to bind control properties in Expression Blend to properties of the sample data via the advanced binding options and see the available properties in the UI.
  4. Specify an optional parameter to the data context that provides variations on the sample.
After many false starts with object providers, converters and other variations I finally hit on a solution that meets all of my requirements.

The first thing we need to do it create a class based on MarkupExtension as follows:
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Markup;
namespace SampleDataPattern.Model
{
/// <summary>
/// This class provides the base for a XAML MarkupExtension that can
/// provide sample data at design time to a property but that leaves
/// alone its value at run time. It is specifically designed to work set the
/// DataContext of a WPF control at design time for testing content,
/// but in theory will work with any dependancy property.
/// </summary>
[MarkupExtensionReturnType(typeof(string))]
public abstract class SampleDataExtension : MarkupExtension
{
private readonly PropertyDescriptorCollection _properties;

/// <summary>
/// Implementing classes must override the constructor to provide the
/// type of the sample data object that will be provided.
/// </summary>
/// <param name="type">The type of the sample data class.</param>
protected SampleDataExtension(Type type)
{
if(_properties != null)
return;

_properties = TypeDescriptor.GetProperties(type);
TypeDescriptor.AddProvider(new SampleDataTypeDescriptionProvider(
_properties), GetType());
}

/// <summary>
/// Implements the ProvideValue method such that if the UIElement that
/// contains the< property is in design mode, the property is left
/// untouched by returning its current value. If it is in design mode we
/// return the object contained by calling the abstract GetSampleObject
/// method.
/// </summary>
public override object ProvideValue(IServiceProvider serviceProvider)
{
var service = (IProvideValueTarget)serviceProvider.GetService(typeof(
IProvideValueTarget));
var control = (UIElement)service.TargetObject;
if (DesignerProperties.GetIsInDesignMode(control))
{
return GetSampleObject();
}
return control.GetValue((DependencyProperty)service.TargetProperty);
}

/// <summary>
/// Derived classes must override this method to return the
/// appropriate sample data.
/// </summary>
protected abstract object GetSampleObject();

/// <summary>
/// Provides a TypeDescriptionProvider that is used to allow Blend to
/// show extension object as if it is the sample object for
/// purposes of binding.
/// </summary>
public class SampleDataTypeDescriptionProvider : TypeDescriptionProvider
{
private readonly PropertyDescriptorCollection _properties;
public SampleDataTypeDescriptionProvider(
PropertyDescriptorCollection properties)
{
_properties = properties;
}

public override ICustomTypeDescriptor GetTypeDescriptor(Type
objectType, object instance)
{
return new SampleTypeDescriptor(_properties);
}
}
/// <summary>
/// Type descriptor returned by SampleDataTypeDescriptionProvider.
/// </summary>
public class SampleTypeDescriptor : CustomTypeDescriptor
{
private readonly PropertyDescriptorCollection _properties;

public SampleTypeDescriptor(PropertyDescriptorCollection properties)
{
_properties = properties;
}

public override PropertyDescriptorCollection GetProperties()
{
return _properties;
}
}

}
}

If we assume you have a class called Person with two properties, FirstName and LastName, you can then extend the extender class with the following sample class:
using SampleDataPattern.Model;

namespace SampleDataPattern.UI.SampleData
{
public class SimpleSample : SampleDataExtension
{
public SimpleSample()
: base(typeof(Person))
{
}

protected override object GetSampleObject()
{
return new Person
{
FirstName = "FirstName",
LastName = "LastName"
};
}
}
}

To use the sample class from you XAML document you can then do the following:
<UserControl x:Class="SampleDataPattern.UI.PersonDisplay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:SampleData="clr-namespace:SampleDataPattern.UI.SampleData"
Height="300" Width="300"
DataContext="{SampleData:SimpleSample}">

<StackPanel Background="White">
<TextBlock Text="{Binding FirstName}"/>
<TextBlock Text="{Binding Path=LastName, Mode=Default}" />
<TextBlock />
</StackPanel>
</UserControl>

In Blend you can set the DataContext in the above example by clicking on the "New" button by the DataContext property of the UserControl and selecting the SimpleSample class from the namespace tree view.

If you want the sample data do change based on a parameter of the sample class, you can create your sample class as follows:
using SampleDataPattern.Model;

namespace SampleDataPattern.UI.SampleData
{
public class SamplePerson : SampleDataExtension
{
private readonly string _parameter;

public SamplePerson() : this(null)
{
}

public SamplePerson(string parameter)
: base(typeof(Person))
{
_parameter = parameter;
}

protected override object GetSampleObject()
{
switch (_parameter)
{
case "Bob":
return new Person
{
FirstName = "Bob",
LastName = "Builder"
};
default:
return new Person
{
FirstName = "Fred",
LastName = "Low"
};
}
}
}
}

You can then use the SamplePerson class as follows:
<UserControl x:Class="SampleDataPattern.UI.ParameterPersonDisplay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:SampleData="clr-namespace:SampleDataPattern.UI.SampleData"
Height="300" Width="300"
DataContext="{SampleData:SamplePerson Bob}">

<StackPanel>
<TextBlock Text="{Binding FirstName}"/>
<TextBlock Text="{Binding LastName}"/>
</StackPanel>
</UserControl>

Since we provide a default constructor as well as one that takes a string, the parameter "Bob" is optional.

The only problems I have found so far with this solution are:
  1. You have to set the parameter for the sample data directly in XAML. I haven't found a way to set this through the Blend UI. (I'm no expert with Blend at this stage)
  2. Windows and Pages do not show binding to the DataContext in the VisualStudio designer. UserControls and other controls types work as expected and controls embedded into Windows and Pages work as expected. Seems to be just a limitation in VisualStudio with showing DataContext in design mode.
Feel free to comment if you have issues, find bugs or can think of improvements.