DataTemplates are essential tools provided by WPF that allow us to describe the visual structure of any type of object.
When declaring a DataTemplate, one typically is doing so with the intention of targeting a specific type (or valid subtype) of object. This is done by setting the DataType property of the DataTemplate to the Type of object we’re targeting.
Because the data templating system matches templates not only against instances of the specified type, but against derived types in the specified type’s hierarchy, DataTemplates are an easy way to visually shape large swathes of your own types of interest…
…As Long as the DataType Isn’t an Interface
WPF’s data templating system does not support the explicit targeting of interface types. If you mean to target a “base type” from which many other types descend, then you will have to make do with a standard object type; an abstract class is the closest thing to an interface that we’re allowed to target with DataTemplates.
This may be received by some as saddening news, as many legitimate object hierarchies exist out there whose members only share a specific interface in common. This limitation of WPF would then force an individual working with such an object hierarchy to create a base type (either to implement or wholly replace the interface) solely to get WPF to play nice with their model.
No one likes to be made to do something seemingly arbitrary and without true purpose; fortunately for us, there is a way we can add interface support to DataTemplates. Before we get into that, however, let’s think as to why WPF’s data templating system lacks support for interfaces.
The lack of interface support was the result of a call made by the early developers of the framework, and I believe it was the right one. When designing a frameworks like the greater .NET Framework or WPF, one must design with the intent of producing a framework which behaves in an expected and stable manner. Adding support for a typical object’s type hierarchy is a straight forward objective, as the type hierarchy will consist of a very well defined order of types that can easily be traversed.
There are too many potential caveats when one starts talking about adding support for interfaces, mainly due to the multiple inheritance angle of interfaces; you can easily get into situations where an object implements two interfaces which are also targeted by two separate DataTemplates.
Luckily for us, we aren’t designing a framework intended for mass consumption, so we can implement support with full awareness of what not to do in conjunction with its use.
Adding Interface Support with a DataTemplateSelector
By creating a DataTemplateSelector and using it, we can effectively define data templates which target interfaces as opposed to only standard objects.
Most of the time when we create a DataTemplateSelector, we design it so it can be equipped with a finite number of different DataTemplates to be doled out at run time based on some business-specific logic. This selector is a bit different, as this selector is meant to offer an alternative way of selecting templates in general; thus, this selector must use all resources found in the tree of the container as well as in the current application’s resources as its source for DataTemplates.
This sort of activity should certainly seem to you to be one that carries potentially negative consequences for performance, as combing through the entire set of loaded resources can indeed be an intensive task. In order to alleviate these concerns, the wisest course of action would be to attempt to use (as much as possible) WPF’s own faculties for searching through all relevant resource dictionaries.
There are a number of methods that allow for searching for specific resources across all, however these are internal. But we don’t need to worry about that, because, like many things in life, simplicity is the key, and we can make use of a very simple and familiar method to achieve our objectives: FindResource. Many people are probably used to just using this method to find a resource by its literal string name; instead of the literal name, we’ll be searching using a specific type of ResourceKey.
Below is the code for the template selector:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /// <summary> /// Provides a data template selector which honors data templates targeting interfaces implemented by the /// data context. /// </summary> public sealed class InterfaceTemplateSelector : DataTemplateSelector { /// <inheritdoc/> public override DataTemplate SelectTemplate( object item, DependencyObject container) { FrameworkElement containerElement = container as FrameworkElement ; if ( null == item || null == containerElement) return base .SelectTemplate(item, container); Type itemType = item.GetType(); IEnumerable < Type > dataTypes = Enumerable.Repeat(itemType, 1).Concat(itemType.GetInterfaces()); DataTemplate template = dataTypes.Select(t => new DataTemplateKey(t)) .Select(containerElement.TryFindResource) .OfType<DataTemplate>() .FirstOrDefault(); return template ?? base .SelectTemplate(item, container); } } |
InterfaceTemplateSelector.cs
This will return the first DataTemplate encountered which targets one of the interfaces implemented by the item, with greater precedence given to templates that specifically target the concrete type of the item. If no templates are found, then the base selection logic will fire. The base selection logic should expand the search to include the rest of the object types in the item’s type hierarchy. Thus, the templates based on their targeted type returned, in order of precedence, is:
- Templates targeting the item’s specific type
- Templates targeting an interface implemented by the item
- Templates targeting other object types found in the item’s type hierarchy.
#2 and #3 may seem a bit unnatural, and that’s because they are. A superior solution would give greater precedence to object types found in the item’s type hierarchy if they too implement the same interfaces. If they do not, and the implementation of the interface is done somewhere in the hierarchy lower than the position of a given ancestor, then the template targeting that interface would take precedence.
Such efforts are largely unnecessary, however, as common sense would dictate that one should only use this selector isn’t meant to data template selection in its entirety; it should be used scenarios where interface is king.