WPF: Allow your users to define their own styles using XAML
An application I’ve been working on has the vague property of “displaying items on the screen”, and a fairly unusual requirement which specifies that users be able to completely define how the items look.
Our items are displayed on a timeline and can represent pretty much anything (up to the user), hence the flexibility required for the visuals.
In order to support this, we do the following:
- Have a generic class (DataItem) that represents a rendered item and which supports a few basic properties (name, start date, etc).
- Support any number of custom properties for that class (stored as an internal collection).
- Allow the user to define a “Style name” for any item (think of a CSS style name).
- Allow them to define a global list of styles (think of a CSS stylesheet), each with custom XAML.
- Allow that XAML to bind to any custom property of an item.
- Lookup the style at runtime, load the XAML then display it in the user control.
There are a few small tricks that make this possible:
Binding To Custom Properties
Because custom properties on an item are just key-value pairs (in our case, strings), we needed a unified way to read any property (whether native or custom) from an item so that XAML binding could use a single syntax.
First, we create a method on our item class that returns the value of any property:
1: public string GetFieldValue(string fieldName)
2: {
3: // First attempt to get an actual item property using reflection.
4: PropertyInfo prop = typeof(DataItem).GetProperties()
5: .SingleOrDefault( p => p.Name.EqualsIgnoreCase(fieldName) );
6: if (prop != null)
7: {
8: object val = prop.GetValue(this, null);
9: return val == null ? "" : val.ToString();
10: }
11:
12: // Failing that, attempt to get it from the custom field collection.
13: if ( _CustomFields.ContainsKey(fieldName) )
14: return _CustomFields[fieldName];
15: // Failing that, just return an empty string.
16: else
17: return "";
18: }
So now we can get the value of an item’s property whether it’s native or custom – but how do we use XAML data binding to bind to the results of this method? Use a Value Converter:
1: /// <summary>
2: /// This takes as a parameter the name of the field to read from the
3: /// DataItem, and returns the value of that field from the bound-to.
4: /// DataItem.
5: /// </summary>
6: public class DataItemPropertyReader : IValueConverter
7: {
8: public object Convert(object value, Type targetType,
9: object parameter, System.Globalization.CultureInfo culture)
10: {
11: return (value as DataItem)
12: .GetFieldValue(parameter.ToString());
13: }
14:
15: public object ConvertBack(object value, Type targetType,
16: object parameter, System.Globalization.CultureInfo culture)
17: {
18: throw new NotImplementedException();
19: }
20: }
Declare an instance of this guy which is app-wide (say, in Styles.xaml):
1: <!-- This is used by individual DataItems to bind to custom properties in items -->
2: <MyNamespace:DataItemPropertyReader x:Key="DI" />
And now the users who write the XAML can bind like this:
1: <TextBlock Text="{Binding Converter={StaticResource DI}, ConverterParameter=Name}" />
2: <TextBlock Text="{Binding Converter={StaticResource DI}, ConverterParameter=Custom1}" />
3: <TextBlock Text="{Binding Converter={StaticResource DI}, ConverterParameter=Address}" />
Displaying Custom XAML Dynamically
Now it comes time to read this XAML and display it. This has been shown before many times, so I won’t go into it too much. It’s fairly simple; the only trip-ups are name-spaces and data contexts.
At runtime, use user control replaces the contents of its layout as such:
1: private void RedrawItem()
2: {
3: string XAML = GetXAMLByStyleName( Item.StyleName );
4:
5: UIElement el = Common.CreateUIElementFromXAML(XAML);
6:
7: // Remove what was there, and add the new stuff.
8: LayoutRoot.Children.Clear();
9: LayoutRoot.Children.Add(el);
10:
11: // Set the context, so that data binding knows
12: // what to look at.
13: LayoutRoot.DataContext = this.Item;
14: }
15:
16: public static UIElement CreateUIElementFromXAML(string XAML)
17: {
18: // Wrap the XAML in a grid, to guarantee one root element
19: XAML = string.Format("<Grid>{0}</Grid>", XAML);
20:
21: byte[] XAMLbytes = System.Text.Encoding.UTF8.GetBytes(XAML);
22: MemoryStream memstream = new MemoryStream(XAMLbytes);
23:
24: // Can only load the XAML if we know about the right namespaces.
25: // (You'd do well to centralize this bit of code)
26: ParserContext pc = new ParserContext();
27: pc.XmlnsDictionary.Add("",
28: "https://schemas.microsoft.com/winfx/2006/xaml/presentation");
29: pc.XmlnsDictionary.Add("x",
30: "https://schemas.microsoft.com/winfx/2006/xaml");
31: pc.XmlnsDictionary.Add("mc",
32: "https://schemas.openxmlformats.org/markup-compatibility/2006");
33:
34: UIElement el = (UIElement)XamlReader.Load(memstream, pc);
35: return el;
36: }
As for performance; it’s not too bad. My app doesn’t need to do this for more than a few dozen items, so we haven’t really pushed it.
Avi