Why would I want to trick Silverlight into treating Dictionary values like real Properties?
There are scenarios when complex, data-intensive applications will need to data bind to keys/value pairs or generally determine the properties of a Class at runtime. In cases where there could be large numbers of keys or the keys could change without an application re-deploy, this is a tricky problem to solve.
In Silverlight 5, however, you can use ICustomTypeProvider to achieve the same goal in a clean fashion.
Data Facets
Some systems, such as Pivot Viewer, allow you to specify any number of pseudo-Properties about interesting items, which we’ll call Facets. Using a structure like a Dictionary, we could specify any number of Facets. Two-way data binding to these is problematic however since they aren’t real CLR properties. In the full .NET Framework you can do tricks with ICustomTypeDescriptor, and now in Silverlight 5 we have System.Reflection.ICustomTypeProvider.
Let’s create a simple Facet class to represent data about the Facet we’d like to data bind to.
/// <summary>
/// Some facet of a dynamic type
/// </summary>
public class Facet
{
/// <summary>
/// Must be a valid CLR property name
/// </summary>
public string PropertyName { get; set; }
public Type PropertyType { get; set; }
//Couple of demo Facets
public static Facet DynamicDemo0 = new Facet
{
PropertyName = "DynamicPropZero",
PropertyType = typeof(string)
};
public static Facet DynamicDemo1 = new Facet
{
PropertyName = "DynamicPropOne",
PropertyType = typeof(double)
};
}
Next we’ll create an object with a regular CLR property and a dictionary to store key/value pairs. These key value pairs will be made binding-friendly.
/// <summary>
/// An Class with normal properties, but also supporting dynamic properties
/// </summary>
public class FacetedObject : ICustomTypeProvider, INotifyPropertyChanged,
INotifyDataErrorInfo
{
Dictionary<string, object> _facetValues;
public object this[string key]
{
get
{
if (!_facetValues.ContainsKey(key))
{
return null;
}
return _facetValues[key];
}
set
{
_facetValues[key] = value;
OnPropertyChanged(key);
}
}
The interesting thing here is the ICustomTypeProvider interface implementation.
ICustomTypeProvider Implementation
On any CLR object you can call GetType(). If you’ve ever done any reflection programming you’re aware of all the rich runtime metadata about your classes that System.Type can provide. System.Type is also an abstract class, and ICustomTypeProvider requires only a single method implementation:
public Type GetCustomType()
{
return new FacetedObjectType<FacetedObject>(_currentFacets);
}
So, we can create a class that extends System.Type and do some interesting things. We can trick the Silverlight runtime into thinking our Facets are real CLR Properties. While my FacetObjectType<TSource> implementation is about 300 lines long, here’s the most interesting part:
/// <summary>
/// A custom System.Type implementation that can provide different Properties at runtime.
All operations except those related to
/// Property logic delegated to the type of TSource
/// </summary>
/// <typeparam name="TSource"></typeparam>
public class FacetedObjectType<TSource> : System.Type
{
//snip…
public override System.Reflection.PropertyInfo[] GetProperties(BindingFlags bindingAttr)
{
var properties = ProxyTargetType.GetProperties(bindingAttr);
if (
BindingFlags.Instance == (bindingAttr & BindingFlags.Instance)
&& BindingFlags.Public == (bindingAttr & BindingFlags.Public)
)
{
var dynamicProperties = GetPublicDynamicProperties();
var allprops = new List<PropertyInfo>();
allprops.AddRange(properties);
allprops.AddRange(dynamicProperties);
return allprops.ToArray();
}
return properties;
}
//snip…
So, for system types that implement ICustomTypeProvider, we can intercept important requests for reflection information and supplement that information. In our case here, we can claim that properties exist that aren’t really on our class at compile time. In terms of telling the runtime how to actually Get and Set these dynamic properties, we need to create a class that extends PropertyInfo. Here’s a type called DynamicPropertyInfo, and the two most interesting methods:
public class DynamicPropertyInfo : PropertyInfo
{
public DynamicPropertyInfo(Type propertyType, Type declaringType, string propertyName)
{
_propertyType = propertyType;
_declaringType = declaringType;
_name = propertyName;
}
public override object GetValue(object obj, BindingFlags invokeAttr, Binder binder,
object[] index, System.Globalization.CultureInfo culture
{
var fo = obj as FacetedObject;
return fo[Name];
}
public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder,
object[] index, System.Globalization.CultureInfo culture)
{
var fo = obj as FacetedObject;
fo[Name] = value;
}
In here, we can just use the this[] indexer of our FacetedObject class to get and set values.
Demo App
To show how this concept works, let’s create a Silverlight 5 application with a DataGrid. We’re going to get the datagrid to display “properties” that technically speaking are not there. First we’ll create a couple of Facets assigned to FacetedObject by default called “DynamicPropZero” and “DynamicPropOne”. For these two we can create DataGridColumns along with an actual compile-time Property of FacetedObject.
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Header="Id" IsReadOnly="True" Binding="{Binding Id}" />
<sdk:DataGridTemplateColumn Header="Dynamic Property 0">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding DynamicPropZero, Mode=TwoWay,
NotifyOnValidationError=True,
ValidatesOnNotifyDataErrors=True, ValidatesOnDataErrors=True,ValidatesOnExceptions=True}"/>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTextColumn Header="Dynamic Property 1"
Binding="{Binding DynamicPropOne, Mode=TwoWay}" />
</sdk:DataGrid.Columns>
Here’s the code we’re using to create the sample data in our main ViewModel. Note that we're going to explose the dictionary values as properties.
public class ShellViewModel : INotifyPropertyChanged
{
public ShellViewModel()
{
Items = new ObservableCollection<FacetedObject>();
var d0 = new FacetedObject();
d0[Facet.DynamicDemo0.PropertyName] = "I'm a property!";
d0[Facet.DynamicDemo1.PropertyName] = 42.42;
var d1 = new FacetedObject();
d1[Facet.DynamicDemo0.PropertyName] = "Would you like to be a property too?";
Items.Add(d0);
Items.Add(d1);
And as you can see, it works:
Notice the buttons above the DataGrid. These dynamic properties wouldn’t be very useful unless they are first-class citizens, and they are. Note that FacetedObject also implements INotifyPropertyChanged and INotifyDataErrorInfo. By clicking the buttons we fire commands that affect changes in code, and the UI reflects the changes for the dynamic properties.
Now, recall at the beginning that I said this could be completely dynamic and that we could actually create both the Facet data and visuals in a data driven fashion at runtime. Clicking on the Add Facet button demonstrates this.
After clicking OK, we can do some work to add this Fact to the items on the ViewModel, and also dynamically create a new DataGridColumn to display the data.
public void AddNewFacet(string name, object defaultValue, string clrType)
{
var typeDict = new Dictionary<string, Type>();
typeDict["string"] = typeof(string);
typeDict["int"] = typeof(int);
typeDict["double"] = typeof(double);
//1. Create a new Facet
var newFacet = new Facet
{
PropertyName = name,
PropertyType = typeDict[clrType]
};
//2. Tell objects to clear out cached state
//3. Assign a default value we can see in the UI
var vm = DataContext as ShellViewModel;
foreach (var item in vm.Items)
{
item.AddFacet(newFacet);
item[newFacet.PropertyName] = defaultValue;
}
//4. create visuals to bind to new facet
var sb = new StringBuilder("<DataTemplate
xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" >")
.Append("<TextBox Text=\"{Binding ")
.Append(name)
.Append(", Mode=TwoWay}\" />")
.Append("</DataTemplate>");
var dt = (DataTemplate)XamlReader.Load(sb.ToString());
var column = new DataGridTemplateColumn();
column.Header = name;
column.CellTemplate = dt;
FacetGrid.Columns.Add(column);
}
Our new DynamicProperty is now shown in the DataGrid and we can edit it with two-way binding support:
For some, this last piece of code will look like the worst kind of voodoo, but there are cases where your requirements will dictate this level of flexibility and this combination of techniques will help you get there.