WPF Control’s Default Style or Template by Extending the WPF Designer in Visual Studio 2010

This article presents how you can get a WPF control’s default style or template by adding functionality right into the WPF designer.

Introduction

Usually, when we want to change the appearance of a control, we will change the control’s template. However, most of the time, we will not create a template from scratch. We get the control’s default template then edit it bit by bit until we obtain the appearance that we want. To get the template, we need to use a tool like Expression Blend. There are also some free standalone applications that can get a control’s default style or template. With Visual Studio 2010, we can extend the WPF designer so we can get the default style or template of a control right within the IDE. Unfortunately, I wasn’t able to find a way how to do the same thing with Visual Studio 2008.

Getting Started

Below is the link to the main WPF designer extensibility reference. I would recommend reading this first for an overview and basic examples. You can see here what other functionalities you can add to the WPF designer.

http://msdn.microsoft.com/en-us/library/bb546938(v=VS.100).aspx

Creating a Context Menu Item

First, we have to add a new item to the context menu of a control. Fist, create a class library project. This automatically creates a class called Class1. Rename this class to Metadata.

using System.Windows.Controls;
using Microsoft.Windows.Design.Features;
using Microsoft.Windows.Design.Metadata;

namespace GetTemplateExtension.VisualStudio.Design
{
// Container for any general design-time metadata to initialize.
// Designers look for a type in the design-time assembly that
// implements IProvideAttributeTable. If found, designers instantiate
// this class and access its AttributeTable property automatically.
internal class Metadata : IProvideAttributeTable
{
// Accessed by the designer to register any design-time metadata.
public AttributeTable AttributeTable
{
get
{
AttributeTableBuilder builder = new AttributeTableBuilder();

// Add the menu provider to the design-time metadata.
builder.AddCustomAttributes(typeof(Control),
new FeatureAttribute(typeof(GetTemplateContextMenuProvider)));

return builder.CreateTable();
}
}
}
}

Listing 1. Class for Registering Design-time Metadata

I got most of the codes here from the WPF designer extensibility reference. You can read the inline comments to better understand what the Metadata class does. Basically, we added a custom attribute for the Control type. This custom attribute is of type FeatureAttribute, which is used for adding a design-time feature for the specified type. The FeatureAttribute constructor takes in as an argument a type that inherits from FeatureProvider. In our case, we created a new class called GetTemplateContextMenuProvider, which is shown in the following code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using GetTemplateExtension.VisualStudio.Design.Dialogs;
using Microsoft.Windows.Design.Interaction;
using Microsoft.Windows.Design.Model;

namespace GetTemplateExtension.VisualStudio.Design
{
class GetTemplateContextMenuProvider : PrimarySelectionContextMenuProvider
{
private MenuAction getDefaultStyleMenuAction;
private MenuGroup getTemplateMenuGroup;
private ModelItem currentModelItem;

// The provider's constructor sets up the MenuAction objects
// and the the MenuGroup which holds them.
public GetTemplateContextMenuProvider()
{
getDefaultStyleMenuAction = new MenuAction("Get Default Style");
getDefaultStyleMenuAction.Execute += GetDefaultStyle_Execute;

getTemplateMenuGroup = new MenuGroup("GetTemplateGroup", "Get Template");
getTemplateMenuGroup.HasDropDown = true;

this.Items.Add(getDefaultStyleMenuAction);
this.Items.Add(getTemplateMenuGroup);

// The UpdateItemStatus event is raised immediately before
// this provider shows its tabs, which provides the opportunity
// to set states.
UpdateItemStatus += new EventHandler<MenuActionEventArgs>(CustomContextMenuProvider_UpdateItemStatus);
}

// The following method handles the UpdateItemStatus event.
void CustomContextMenuProvider_UpdateItemStatus(object sender, MenuActionEventArgs e)
{
ModelItem controlModelItem = e.Selection.PrimarySelection;

// Checking to prevent duplicate menu items, which happens when user
// switch to other window and back.
if (controlModelItem == currentModelItem)
return;

currentModelItem = controlModelItem;

// Get properties that are templates.
IEnumerable<PropertyInfo> templatePropertyInfos = controlModelItem.ItemType.GetProperties(
BindingFlags.Instance | BindingFlags.Public).Where(p => p.PropertyType.IsSubclassOf(typeof(FrameworkTemplate)));

foreach (PropertyInfo templatePropertyInfo in templatePropertyInfos)
{
MenuAction menuAction = new MenuAction(templatePropertyInfo.Name);
menuAction.Execute += GetTemplate_Execute;
getTemplateMenuGroup.Items.Add(menuAction);
}
}

// The following method handles the Execute event.
// It sets the Background property to its default value.
void GetDefaultStyle_Execute(object sender, MenuActionEventArgs e)
{
ModelItem controlModelItem = e.Selection.PrimarySelection;
Control control = (Control)controlModelItem.GetCurrentValue();

StyleWindow window = new StyleWindow(control);
window.ShowDialog();
}

// The following method handles the Execute event.
// It sets the Background property to its default value.
void GetTemplate_Execute(object sender, MenuActionEventArgs e)
{
ModelItem controlModelItem = e.Selection.PrimarySelection;
Control control = (Control)controlModelItem.GetCurrentValue();

MenuAction menuAction = (MenuAction)sender;
TemplateWindow window = new TemplateWindow(control, menuAction.DisplayName);
window.ShowDialog();
}
}
}

Listing 2. The Menu Provider

Our class inherits from PrimarySelectionContextMenuProvider. This base class provides us a way to show a group of context menu items for the current selection. The Items property, inherited from ContextMenuProvider, contains the MenuBase objects that will be shown in the context menu. As you can see in the constructor in Listing 2, we added to the Items collection one MenuAction object and one MenuGroup object: “Get Default Style” and “Get Template.”

The “Get Default Style” MenuAction shows a window containing the selected control’s default style. Meanwhile, the “Get Template” MenuGroup does not contain any MenuAction object yet. We will fill up the MenuGroup with the selected control’s properties that are templates. Since the selected control changes, we can only do this in the UpdateItemStatus event handler. The UpdateItemStatus event is raised before the context menu is shown. In the event handler, we can identify which properties of the control derive from FrameworkTemplate.

Loading Design-Time Metadata

We now have our assembly that contains additional context menu items for WPF controls. Our problem now is how to load the metadata in Visual Studio. Karl Shifflett has great posts on this topic. These are found on the following links.

http://blogs.msdn.com/wpfsldesigner/archive/2010/01/13/wpf-silverlight-design-time-code-sharing-part-i.aspx

http://blogs.msdn.com/wpfsldesigner/archive/2010/01/13/loading-metadata-for-microsoft-controls.aspx

The first link details how to load design-time metadata for custom controls you authored. Basically, there is a naming convention for an assembly containing the metadata, and the assembly should be on the same folder (or Design sub-folder) as that of the assembly containing the custom control. In our example, the assembly is named GetTemplateExtension.VisualStudio.Design. Using the naming convention, you can specify whether your metadata is available for Visual Studio, Expression Blend, or both.

As you might remember, we did not create a custom control. We only added a custom attribute for the Control type. Thus, we are more interested in the second link. The link details how to load the metadata for existing controls like Button, CheckBox, and ComboBox. This involves adding registry entries. The following figure shows the registry entries used in our example.


Figure 1. Registry Entries Used in Loading Design-Time Metadata

These registry entries are located in HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0\DesignerPlatforms\.NetFramework\Metadata\XamlDesigner\EditTemplateExtension.VisualStudio.Design, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null. To test if this works, I added a window containing different controls in one of the projects in the solution.



Figure 2. Loaded Context Menu Items

As you can see in the figure, we now have our custom menu items added to the WPF designer context menu. The currently selected control is a ComboBox. When we click on the “Get Template” menu, we will see that it has four public properties that derive from FrameworkTemplate.

Showing the Default Style

When we click on the “Get Default Style” menu item, an instance of a StyleWindow class will be shown. Its constructor accepts a parameter of type Control. When the window is shown, it displays the default style of the control. Since we can’t select all types of controls in the WPF designer, I decided to add a TreeView containing all the types that derive from Control, and are found in the current control’s assembly. Selecting another control type in the tree changes the displayed style on the right.


Figure 3. Default Style Window

The control that I used to display the style is called AvalonEdit, a WPF Text Editor by Daniel Grunwald. You can check his CodeProject article regarding AvalonEdit here http://www.codeproject.com/KB/edit/AvalonEdit.aspx. The reason I used this is because of its automatic support for XML syntax highlighting and code folding. The following code listing shows the StyleWindow code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using ICSharpCode.AvalonEdit.Folding;
using ICSharpCode.AvalonEdit.Highlighting;

namespace GetTemplateExtension.VisualStudio.Design.Dialogs
{
/// <summary>
/// Interaction logic for StyleWindow.xaml
/// </summary>
public partial class StyleWindow : Window
{
private Control control;
private FoldingManager foldingManager;

public StyleWindow(Control control)
{
InitializeComponent();

this.control = control;

// Set text editor hightlighting and code folding
textEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");

BuildTree();
}

private void BuildTree()
{
Assembly assembly = control.GetType().Assembly;

// Get public controls and have default constructors
IEnumerable<Type> controlTypes = assembly.GetTypes().Where(
t => t.IsSubclassOf(typeof(Control)) && t.IsPublic && t.GetConstructor(Type.EmptyTypes) != null);

// Get namespaces so we can build a tree like MSDN Library
List<string> namespaces = new List<string>();
foreach (Type controlType in controlTypes)
{
if (!namespaces.Contains(controlType.Namespace))
namespaces.Add(controlType.Namespace);
}
namespaces.Sort();

TreeViewItem prevTreeViewItem = null;
foreach (string controlNamespace in namespaces)
{
// Make the namespaces TreeViewItem controls.
TreeViewItem treeViewItem = new TreeViewItem();
treeViewItem.Header = controlNamespace;

// Find the place where to insert the TreeViewItem
TreeViewItem parentTreeViewItem = prevTreeViewItem;
while (parentTreeViewItem != null &&
!treeViewItem.Header.ToString().StartsWith(parentTreeViewItem.Header.ToString()))
{
parentTreeViewItem = parentTreeViewItem.Parent as TreeViewItem;
}

if (parentTreeViewItem != null)
parentTreeViewItem.Items.Add(treeViewItem);
else
controlsTreeView.Items.Add(treeViewItem);

prevTreeViewItem = treeViewItem;

// Get the controls that are in the current namespace and add them to the TreeViewItem
foreach (Type type in controlTypes.Where(ct => ct.Namespace == controlNamespace).OrderBy(t => t.Name))
{
TreeViewItem controlTreeViewItem = new TreeViewItem();
controlTreeViewItem.Header = type.Name;
controlTreeViewItem.Selected += ControlTreeViewItem_Selected;

treeViewItem.Items.Add(controlTreeViewItem);

if (control.GetType() == type)
{
controlTreeViewItem.IsSelected = true;
treeViewItem.ExpandSubtree();
}
}
}
}

private void ControlTreeViewItem_Selected(object sender, RoutedEventArgs e)
{
Assembly assembly = control.GetType().Assembly;

// make an instance of the type
TreeViewItem leaf = (TreeViewItem)sender;
TreeViewItem parent = (TreeViewItem)leaf.Parent;
Type type = assembly.GetType(parent.Header + "." + leaf.Header);
Title = type.AssemblyQualifiedName;

// get the type's default style key
FrameworkElement element = (FrameworkElement)Activator.CreateInstance(type);
PropertyInfo defaultStyleKeyPropertyInfo = element.GetType().GetProperty(
"DefaultStyleKey", BindingFlags.Instance | BindingFlags.NonPublic);
object defaultStyleKey = defaultStyleKeyPropertyInfo.GetValue(element, null);

// get the default style using the key
Style style = Application.Current.TryFindResource(defaultStyleKey) as Style;

try
{
textEditor.Text = XamlHelper.GetFormattedXaml(style);
}
catch (Exception ex)
{
textEditor.Text = ex.ToString();
}

UpdateTextEditorFolding();
}

private void UpdateTextEditorFolding()
{
if (foldingManager != null)
FoldingManager.Uninstall(foldingManager);

foldingManager = FoldingManager.Install(textEditor.TextArea);
XmlFoldingStrategy foldingStrategy = new XmlFoldingStrategy();
foldingStrategy.UpdateFoldings(foldingManager, textEditor.Document);
}
}
}

Listing 3. StyleWindow Class

In the ControlTreeViewItem_Selected method, we create an instance of the type that is currently selected in the tree. We then obtain the default style key using reflection, as it is not a public member. To find the style, we call the TryFindResource method of the Application object. After this, we can now convert the style into XAML using the static GetFormattedXaml helper method. This method formats the XAML by adding indentation and new lines on attributes.

using System.IO;
using System.Linq;
using System.Windows.Markup;
using System.Xml;
using System.Xml.Linq;

namespace GetTemplateExtension.VisualStudio.Design.Dialogs
{
static class XamlHelper
{
internal static string GetFormattedXaml(object obj)
{
string objXaml = XamlWriter.Save(obj);
XElement rootElement = XElement.Parse(objXaml);

XmlWriterSettings settings = new XmlWriterSettings();
settings.OmitXmlDeclaration = true;
settings.NewLineOnAttributes = true;
settings.Indent = true;
settings.IndentChars = " ";

using (MemoryStream stream = new MemoryStream())
{
using (XmlWriter xmlWriter = XmlWriter.Create(stream, settings))
{
rootElement.WriteTo(xmlWriter);
}

stream.Flush();
stream.Position = 0;
StreamReader reader = new StreamReader(stream);
string formattedXaml = reader.ReadToEnd();

// attributes that are XML namespaces are not put on next line by default
foreach (XAttribute attribute in rootElement.Attributes().Where(a => a.IsNamespaceDeclaration))
{
int index = formattedXaml.IndexOf(attribute.ToString());
formattedXaml = formattedXaml.Insert(index, "\n ");
}

return formattedXaml;
}
}
}
}

Listing 4. XAML Formatting

I put this method in a separate helper class because this is also used in the TemplateWindow, the window responsible for showing a control’s template.

Showing a Template

There’s not much difference between the StyleWindow and TemplateWindow. There’s also a tree on the left side, but it displays the current control’s public properties that derive from FrameworkTemplate. Clicking on an item shows the template on the right side.

Figure 4. Template Window

When a template is currently not set, then the text editor will display the string “Template is not set.” The following code shows the TemplateWindow class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using ICSharpCode.AvalonEdit.Folding;
using ICSharpCode.AvalonEdit.Highlighting;

namespace GetTemplateExtension.VisualStudio.Design.Dialogs
{
/// <summary>
/// Interaction logic for TemplateWindow.xaml
/// </summary>
public partial class TemplateWindow : Window
{
private Control control;
private string templateName;
private FoldingManager foldingManager;

public TemplateWindow(Control control, string templateName)
{
InitializeComponent();

this.control = control;
this.templateName = templateName;

textEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");

BuildTree();
}

private void BuildTree()
{
// Get properties that are templates.
IEnumerable<PropertyInfo> templatePropertyInfos = control.GetType().GetProperties(
BindingFlags.Instance | BindingFlags.Public).Where(p => p.PropertyType.IsSubclassOf(typeof(FrameworkTemplate)));

foreach (PropertyInfo templatePropertyInfo in templatePropertyInfos)
{
TreeViewItem templateTreeViewItem = new TreeViewItem();
templateTreeViewItem.Header = templatePropertyInfo.Name;
templateTreeViewItem.Selected += TemplateTreeViewItem_Selected;

templatesTreeView.Items.Add(templateTreeViewItem);

if (templatePropertyInfo.Name == templateName)
templateTreeViewItem.IsSelected = true;
}
}

private void TemplateTreeViewItem_Selected(object sender, RoutedEventArgs e)
{
TreeViewItem treeViewItem = (TreeViewItem)sender;
string templateName = (string)treeViewItem.Header;
Title = control.GetType().AssemblyQualifiedName + " [" + templateName + "]";

PropertyInfo templatePropertyInfo = control.GetType().GetProperty(
templateName, BindingFlags.Instance | BindingFlags.Public);
object template = templatePropertyInfo.GetValue(control, null);

if (template != null)
{
try
{
textEditor.Text = XamlHelper.GetFormattedXaml(template);
}
catch (Exception ex)
{
textEditor.Text = ex.ToString();
}
}
else
{
textEditor.Text = "Template is not set.";
}

UpdateTextEditorFolding();
}

private void UpdateTextEditorFolding()
{
if (foldingManager != null)
FoldingManager.Uninstall(foldingManager);

foldingManager = FoldingManager.Install(textEditor.TextArea);
XmlFoldingStrategy foldingStrategy = new XmlFoldingStrategy();
foldingStrategy.UpdateFoldings(foldingManager, textEditor.Document);
}
}
}

Listing 5. TemplateWindow Class

You can download the solution here. Don’t forget to set the registry values needed to load the metadata.

By Michael Detras   Popularity  (6423 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog