Simplifying StyleSelector and TemplateSelector for Xceed AvalonDock





5.00/5 (3 votes)
How to make it easier to manage StyleSelector and TemplateSelector with AvalonDock, especially when the selection is based on the ViewModel type only.
Introduction
Like many, I am using the powerful AvalonDock from Xceed to build powerful EDI. And like many, I struggle with the lack of documentation. The most advanced documentation I found is here on codeproject.com and is based on EDI. And I first worked with the PaneStyleSelector
and PaneTemplateSelector
as designed in EDI. But I found this a bit heavy, and managed to build up a lighter version that I want now to share with you.
Background
When it comes to providing a StyleSelector
and a TemplateSelector
, you can see in the article series mentioned above that it involves:
- In the selector itself, a property for each style / template to implement
- A new case in the
SelectStyle
/SelectTemplate
method to map the item to the property - In the XAML part, assigned to the property, the actual style or template.
This provides flexibility to implement any kind of logic to map an item to a style or template. But the cost for that is that the mapping list is hardcoded three times:
- As a set of properties
- As a mapping from items to properties
- As a mapping from properties to styles/templates
And in EDI, like in any implementation I've gone through so far, this mapping has always been based on the item type only.
So I decided to reduce this to one single list, with the assumption that I would only use item types for my mapping.
The New StyleSelector
The styles and templates are coded on the XAML side. So if the list is implemented only once, this is the place. Therefore, we would need a generic place to store this information in the Selector. Let's go for the StyleSelector
and build the appropriate tool:
internal partial class PaneStyleSelector : StyleSelector
{
/// <summary>
/// The mapping between a type and a style.
/// </summary>
public Dictionary<Type, Style> Styles { get; set; } = new Dictionary<Type, Style>();
}
We now have to implement the mapping logic:
/// <summary>
/// Returns the style to use based on the given item's type.
/// </summary>
/// <param name="item">The item to select a style for.</param>
/// <param name="container">Ignored.</param>
/// <returns>The style to apply for this item.</returns>
public override Style SelectStyle(object item, DependencyObject container)
{
var itemType = item.GetType();
foreach (Type t in Styles.Keys)
{
if (itemType.Equals(t) || itemType.IsSubclassOf(t))
return Styles[t];
}
return base.SelectStyle(item, container);
}
In this implementation, the mapping condition...
if (itemType.Equals(t) || itemType.IsSubclassOf(t))
...enables the usage of inheritance. That is, if you have different types of documents that inherit from the same Document
superclass - or even interface
- , and you want them all to have the same style mapped, you can simply map the Document
type to that style.
So let's see how this goes on the XAML side:
<local:PaneStyleSelector x:Key="PaneStyleSelector">
<local:PaneStyleSelector.Styles>
<Style x:Key="{x:Type documents:IDocument}" TargetType="{x:Type xcad:LayoutItem}">
<... />
</Style>
<Style x:Key="{x:Type local:ToolViewModel}" TargetType="{x:Type xcad:LayoutAnchorableItem}">
<... />
</Style>
</local:PaneStyleSelector.Styles>
</local:PaneStyleSelector>
One Point of Attention
The order in a dictionary
is uncertain, and may not be the order in which you entered the items. Imagine you want all your Document
s to use StyleD
, except for the ConfidentialDocument
type (that also inherits from Document
) that should use StyleC
. You cannot assume that putting an entry for ConfidentialDocument
in the dictionary
, and then an entry for Document
will get you to the expected result: it can happen that the order is reversed in the dictionary and that StyleD
is used for ConfidentialDocument
.
There are several ways to solve this issue:
- Adding intelligence to the mapping procedure:
if (item is ConfidentialDocument) return Styles[typeof(ConfidentialDocument)]; else foreach (Type t in Styles.Keys) ...
- Tweaking the
Document
classes hierarchy: you could implement aNonConfidentialDocument
class that would inherit fromDocument
and contain all children classes butConfidentialDocument
, and map the style on this type instead ofDocument
. You may be likely to treat non-confidential document differently in other parts of your code. - Get back to the original property-based version. I wouldn't.
What's Next
Now you get the idea for the StyleSelector
, it is easy to transpose this to the TemplateSelector
.
internal partial class PaneTemplateSelector : DataTemplateSelector
{
/// <summary>
/// The link between a type and a template.
/// </summary>
public Dictionary<Type, DataTemplate>
Templates { get; set; } = new Dictionary<Type, DataTemplate>();
/// <summary>
/// Returns the appropriate template based on the given item's type.
/// </summary>
/// <param name="item">The item to select a template for.</param>
/// <param name="container">Ignored.</param>
/// <returns>The style to apply for this item.</returns>
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var itemType = item.GetType();
foreach (Type t in Templates.Keys)
{
if (itemType.Equals(t) || itemType.IsSubclassOf(t))
return Templates[t];
}
return base.SelectTemplate(item, container);
}
}
And given that the selectors implementations are purely generic, you can simply store this in your favorite library that you will reference in your XAML code. And everything in your application will be in the XAML.
Have fun!
History
- 08.25.2017: Initial version