In my previous post, WPF & Silverlight Design-Time Code Sharing – Part I, I introduced our custom controls and the required design-time features. I also covered how Visual Studio 2010 discovers and loads control design-time assemblies. In addition, I explained how to implement platform neutral WPF & Silverlight type resolution in a common design-time assembly.
In this post, I’ll tie the design-time metadata to each of the design-time features. This post will be more on how you can implement features in your design-times rather than a detailed analysis of the sample solution since the sample solution code has detailed comments and a walk-through the code using the Visual Studio Task List as the navigator for the code.
I have provided many external links in this post and suggest that you read them to ensure you have the required understanding of design-time extensibility for WPF & Silverlight Designer.
I have updated the code download for the WPF & Silverlight Design-Time Code Sharing – Part I. If you previously downloaded the code, please re-download the updated code from this post or Part I. I made a change to the FeedbackControlInitializer.cs that I’ll point out in the Control Default Initializer section below along with a few very minor changes to comments.
To follow along, open the sample solution and view the CiderControlsAttributeTableBuilder.cs file in the CiderControls.Common.VisualStudio.Design
project.
Metadata Loading Revisited
If you read the referenced MSDN pages in the example code project, you will notice that some MSDN examples load metadata by applying a design-time attribute to the run-time control. In other words, the design-time metadata is in the control assembly and not in a separate design-time assembly. These MSDN examples were written to show you that you “can” load metadata this way, but this is not considered a best practice.
For example, view the DefaultInitializer Class MSDN page. You will notice in article’s example code that the default initializer metadata has been added to the Button
’s class declaration using the Feature attribute.
It’s recommended that control developers place their design-time metadata in a design-time assembly and not in the control assembly. Having design-time metadata prevents your run-time controls from having a reference to Microsoft design assemblies. Additionally, if you do not have separate design-time assemblies, you by-pass the built-in metadata loading feature of allowing Visual Studio and Blend to have common as well as separate code. Another advantage of having your design-time code in design assemblies is that you can ship new design assemblies without having to ship a new version of your control assembly.
Please read the Metadata Store MSDN topic for a full discussion of metadata.
Control Default Initializer
Control default initializers are the proper way to assign initial design-time values to properties when a control is created on the design surface using the ToolBox.
The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata for the Feedback control’s control initializer.
new FeatureAttribute(typeof(FeedbackControlInitializer))
Control default initializers must derive from DefaultInitializer and override the InitializeDefaults method as in the below example.
using CiderControls.Common.VisualStudio.Design.Infrastructure;
using Microsoft.Windows.Design.Model;
namespace CiderControls.Common.VisualStudio.Design.Controls {
internal class FeedbackControlInitializer : DefaultInitializer {
public FeedbackControlInitializer() {
}
public override void InitializeDefaults(ModelItem item) {
base.InitializeDefaults(item);
item.Properties[MyPlatformTypes.Feedback.CornerRadiusProperty].SetValue("10");
item.Properties[MyPlatformTypes.Feedback.BorderThicknessProperty].SetValue("2");
item.Properties[MyPlatformTypes.Feedback.BorderBrushProperty].SetValue("LightGray");
}
}
}
You can access a property in the ModelItem.Properties
collection by using either the name of the property or by using property identifier as I have done above. Using a property identifier removes quoted strings from your code and IntelliSense provides easy access to defined property identifiers.
The SetValue method takes a parameter of type object and converts values as required. Notice how “10
” is converted to a CornerRadius
, “2
” is converted to a Thickness
and “LightGray
” is converted to a SolidColorBrush
object.
ModelItem SetValue Method
In the original code download, I had a mistake by trying to set the above CornerRadius
value using the syntax .SetValue(10)
instead of the correct syntax .SetValue(“10”)
. Visual Studio 2010 Beta2 swallowed the exception. All future versions of Visual Studio 2010 will report this as an exception, which is the correct behavior. I have updated the code download with the corrected code.
SetValue
takes an object as the method parameter. If a string
is passed, a type converter will be used to assign the value as in the above code. Type converters are also used to convert string
values in XAML files to property values.
If a string
is not passed, an object that matches the type of the property must be passed.
Example, property type is Integer
, using .SetValue(10)
or .SetValue(“10”)
is correct.
Example, property type is Thickness
, using .SetValue(10)
is not correct because 10
is not a Thickness
. The correct way to pass this parameter is to use (“10
”) which will be converted to a Thickness
. You can also pass in a platform specific Thickness
object instead of (“10
”).
Control Context Menu
A design-time context menu can be added to controls on the design surface. You can use context menus for many purposes such as setting control values or opening a dialog.
The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata for the Feedback control’s context menu.
new FeatureAttribute(typeof(FeedbackControlContextMenuProvider))
To implement a context menu provider, your class must derive from ContextMenuProvider or a class that derives from ContextMenuProvider
. In the example solution, I have derived my ContextMenuProvider
from PrimarySelectionContextMenuProvider. PrimarySelectionContextMenuProvider
adds additional functionality to ContextMenuProvider
by automatically displaying the context menu when the control is selected on the design surface when the developer right clicks the control.
Implementing a context menu is very simple. In the constructor, add a MenuAction for each desired context menu item to your class’s Items collection. You can add one or more MenuActions
and MenuActions
can be nested. MenuActions
can also be marked as Checkable
as in the above image.
The UpdateItemStatus
event is raised just before the context menu item is displayed. At your option, you can hide or display items, enable or disable items and if the menu item is Checkable
, set it as Checked
or not. The sample solution uses UpdateItemStatus
to Check the MenuAction
that represents the current value of the Feedback control.
When adding the MenuAction
, you must also provide implementation for the MenuAction Execute
event. The Execute
event is raised when the menu item is clicked.
The below FeedbackControlContextMenuProvider
constructor demonstrates setting up the handler for UpdateItemStatus
event, the creation of a sub menu and adding items to it.
public FeedbackControlContextMenuProvider() {
this.UpdateItemStatus +=
new EventHandler<MenuActionEventArgs>
(FeedbackControlContextMenuProvider_UpdateItemStatus);
_feedbackGroup = new MenuGroup(Constants.STR_FEEDBACKGROUP, Strings.MenuFeedbackSetValue);
_feedbackGroup.HasDropDown = true;
for (int i = Constants.INT_MINIMUMRATINGVALUE;
i < Constants.INT_MAXIMUMRATINGVALUE + 1; i++) {
FeedbackMenuAction menuItem =
new FeedbackMenuAction(
Strings.ResourceManager.GetString(string.Format("MenuFeedbackSetValue{0}", i)), i);
menuItem.Checkable = true;
_feedbackGroup.Items.Add(menuItem);
menuItem.Execute += new EventHandler<MenuActionEventArgs>
(FeedbackSetValueMenuAction_Execute);
}
this.Items.Add(_feedbackGroup);
}
When the HasDropDown
property is true
, it will display its sub items in a fly out menu. If false
, the sub items will be listed inline in the context menu.
To make a context menu item (MenuAction
) Checkable
, set the Checkable
property to true
. To check a menu item, set the Checked
property to true
.
Set up an event handler for each menu item’s Execute
event. This code will be called when the menu item is clicked at design-time.
Control Adorner
Design-time control adorners can be used to provide additional UIElement
s on the design surface adding features like selection handles, grid lines, grid rails, buttons or other design-time features that your control requires at design-time. The display of an adorner is controlled by a SelectionPolicy. I recommend that you read up on the base class that all policies like SelectionPolicy
derive from, ItemPolicy.
MSDN has a great two adorner articles, Adorner Architecture and AdornerProvider Class that provide a solid understanding of adorners. These articles also have several links to example code for implementing an adorner.
In the below image, the Feedback control design-time exposes a Rating Control in an adorner to provide a design-time interactive way to set the value of the Feedback control. The lower set of 3 green ellipses and 2 black ellipses is the adorner.
The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata for the Feedback control’s adorner.
new FeatureAttribute(typeof(FeedbackControlAdornerProvider))
To implement an adorner, your class must derive from AdornerProvider
or a class that derives from AdornerProvider
like PrimarySelectionAdornerProvider
.
The adorner in the sample solution derives from AdornerProvider
. Using this class instead PrimarySelectionAdornerProvider
requires that we do more work to control when our adorner is visible, but this also gives us more control over when the adorner is displayed.
PrimarySelectionAdornerProvider
will display the adorner when the control is selected on the design surface. This would be desirable for adorners like resize, control alignment, grid rails or control size label adorners.
However, our adorner is only used to set a control value so we didn’t want this adorner showing when the Feedback control is being resized or moved. This was accomplished by deriving from AdornderProvider
and applying a SelectionPolicy
to the Adorner
class.
The below FeedbackControlAdornerProvider
code is an example shell for an adorner. I have stripped out the implementation code for clarity so we can focus on writing an adorner. The solution sample code has comments for how I implemented the adorner in the sample.
The UsesItemPolicy attribute is used to associate a policy with an adorner. The policy determines when the adorner is visible.
The IsToolSupported method allows you to inform the Designer if your adorner supports the currently selected tool or not. Your Adorner will not be displayed if the adorner does not support the current tool. The Visual Studio 2010 WPF & Silverlight Designer ships with two tools, CreationTool and the SelectionTool. The below IsToolSupported code demonstrates how to indicate that the adorner does not support the CreationTool.
The Activate method can be used to create your adorner and add required event handlers. At your option, you can elect to create your adorner each time Activate is called or you can create the adorner once here or in the adorner constructor and maintain a module level reference to it.
The Deactivate method is used to unhook any event handlers added in the Activate
method.
Please beware that Activate
and Deactivate
may be called several times during the adorner lifetime.
[UsesItemPolicy(typeof(FeedbackControlSelectionPolicy))]
internal class FeedbackControlAdornerProvider : AdornerProvider {
public override bool IsToolSupported(Tool tool) {
if (tool is SelectionTool) {
return true;
}
return false;
}
protected override void Activate(ModelItem item) {
base.Activate(item);
}
protected override void Deactivate() {
base.Deactivate();
}
}
Controlling Adorner Display
The below FeedbackControlSelectionPolicy
class is a generic implementation of a selection policy that displays the adorner when the control is selected, but hides it when the control is being moved or resized. You can reuse this selection policy in your applications to get the same behavior.
The below code is commented in detail:
using Microsoft.Windows.Design.Interaction;
using Microsoft.Windows.Design.Model;
using Microsoft.Windows.Design.Policies;
namespace CiderControls.Common.VisualStudio.Design.Controls {
internal class FeedbackControlSelectionPolicy : PrimarySelectionPolicy {
private bool _isFocused;
public FeedbackControlSelectionPolicy() {
}
protected override bool IsInPolicy(Selection selection, ModelItem item) {
bool inPolicy = base.IsInPolicy(selection, item);
return inPolicy && !_isFocused;
}
protected override void OnActivated() {
this.Context.Items.Subscribe<FocusedTask>(OnFocusedTaskChanged);
base.OnActivated();
}
protected override void OnDeactivated() {
this.Context.Items.Unsubscribe<FocusedTask>(OnFocusedTaskChanged);
base.OnDeactivated();
}
private void OnFocusedTaskChanged(FocusedTask f) {
bool nowFocused = f.Task != null;
if (nowFocused != _isFocused) {
_isFocused = nowFocused;
Selection selection = Context.Items.GetValue<Selection>();
if (selection.PrimarySelection != null) {
ModelItem[] removed;
ModelItem[] added;
if (nowFocused) {
removed = new ModelItem[] { selection.PrimarySelection };
added = new ModelItem[0];
} else {
removed = new ModelItem[0];
added = new ModelItem[] { selection.PrimarySelection };
}
OnPolicyItemsChanged(new PolicyItemsChangedEventArgs(this, added, removed));
}
}
}
}
}
Category Editor
Category editors are used in the properties window category view to provide a custom UI for editing related properties in a specific category. The Text category editor is a good example of a category editor. Category editors are implemented as DataTemplates
. DataTemplates
provide developers the freedom to implement any UI for editing properties.
I strongly recommend that you read the MSDN topics, Property Editing Architecture and Property Editing Namespace. These will provide you an outstanding overview of property editing.
The Feedback control has a Custom category editor that is pictured above. This editor allows the four custom properties exposed by the Feedback control to be edited as a group.
You can include the PropertyMarker
in your category editors as I have done above if you desire. The PropertyMarker
provides a lot of free functionality such as applying a data binding, applying a resource, extracting a value to a resource or resetting the property value.
Properties can be edited using the default PropertyValueEditor or a custom PropertyValueEditor
. Notice the above Value
property has a custom PropertyValueEditor
. See the Property Value Editor section below for registering and implementing a PropertyValueEditor
.
Examples of a default PropertyValueEditor
are TextBox
es for strings or numbers, CheckBox
es for Booleans and ComboBox
es for properties of an enum
type. If you do not assign a custom PropertyValueEditor
, the Designer will select the most appropriate PropertyValueEditor
for you without any action on your part. In the above category editor, the first three properties are using the default PropertyValueEditor
.
A custom PropertyValueEditor
is used to provide a custom UI for setting a value on the property. The above category editor Value
property is edited using the Rating control.
The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata for the Feedback control’s category editor.
AddCategoryEditor(feebackType, typeof(FeedbackControlCategoryEditor));
To implement a category editor, your class must derive from CategoryEditor.
When the ConsumesProperty method returns true
, this indicates that the property is included in the category editor and won’t be listed outside of the category editor. If ConsumesProperty
returns false
, the property will be listed as a separate row within the category but outside the category editor. This category editor provides editing for all properties in the Custom category so ConsumesProperty
always returns true
.
The EditorTemplate property returns a DataTemplate
that will act as the UI for the category editor. The DataTemplate
will have its DataContext
set to a CategoryEntry.
The TargetCategory property identifies which category this category editor applies to.
namespace CiderControls.Common.VisualStudio.Design.Controls {
internal class FeedbackControlCategoryEditor : CategoryEditor {
public FeedbackControlCategoryEditor() {
}
public override bool ConsumesProperty(PropertyEntry propertyEntry) {
return true;
}
public override DataTemplate EditorTemplate {
get {
return FeedbackControlResourceDictionary.Instance.FeedbackCategoryEditor;
}
}
public override object GetImage(Size desiredSize) {
return null;
}
public override string TargetCategory {
get { return Constants.STR_CUSTOM; }
}
}
}
EditorTemplate
The EditorTemplate
is a DataTemplate
stored in a resource dictionary. The below code-behind file for the FeedbackControlResourceDictionary
demonstrates one technique for conserving resources at design-time by exposing the resource dictionary as a Singleton and also how to expose one or more DataTemplate
s as strongly typed properties.
The above EditorTemplate
property illustrates strong type access to the FeedbackCategoryEditor DataTemplate
through the Instance
property. The Instance
property provides Singleton design pattern access to the resource dictionary.
If the resource dictionary has more that one DataTemplate
, you would add additional properties to expose additional DataTemplates
.
Notice the constant STR_FEEDBACKCATEGORYEDITORTEMPLATE
is used in the FeedbackCategoryEditor
property. This same constant is also used in the below FeedbackControlResourceDictionary DataTemplate Key
. Using constants improves the maintainability of your code by removing quoted strings, while IntelliSense provides quick access to your defined constants when editing code.
namespace CiderControls.Common.VisualStudio.Design.Controls {
internal partial class FeedbackControlResourceDictionary : ResourceDictionary {
[ThreadStatic]
private static FeedbackControlResourceDictionary _instance;
private FeedbackControlResourceDictionary() {
InitializeComponent();
}
internal static FeedbackControlResourceDictionary Instance {
get {
if (_instance == null) {
_instance = new FeedbackControlResourceDictionary();
}
return _instance;
}
}
public DataTemplate FeedbackCategoryEditor {
get {
return this[Constants.STR_FEEDBACKCATEGORYEDITORTEMPLATE] as DataTemplate;
}
}
}
}
Category Editor DataTemplate
The below DataTemplate
is a Grid with four rows each containing a PropertyContainer control.
A PropertyContainer
control gives you several options for specifying the type of UI editor you want for each property. I have chosen to implement the UI using an InlineRowTemplate
.
In the below XAML, you’ll see Binding MarkupExtensions
with ( )
and [ ]
used when assigning the Path. If these are unfamiliar to you, please read the MSDN topic Binding Declarations Overview. ( ) used in a Path
is for attached properties. [ ]
used in a Path
is for indexers.
<DataTemplate
x:Key="{x:Static c:Constants.STR_FEEDBACKCATEGORYEDITORTEMPLATE}">
<Border Background="Wheat" Padding="6,0,6,6">
<Grid>
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="10" />
<Setter Property="VerticalAlignment" Value="Bottom" />
</Style>
<!-- this ControlTemple is used by all the properties at the bottom of this file -->
<ControlTemplate x:Key="editorTemplate">
<Grid
Margin="0,7,0,0"
DataContext="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=(mwdpe:PropertyContainer.OwningPropertyContainer)}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Path=PropertyEntry.PropertyName}" />
<mwdpe:PropertyMarker Grid.Row="1" VerticalAlignment="Center"
HorizontalAlignment="Left" />
<ContentPresenter
Margin="20,0,0,0" Grid.Row="1" VerticalAlignment="Center"
Content="{Binding Path=PropertyEntry.PropertyValue}"
ContentTemplate="{Binding RelativeSource={RelativeSource Self},
Path=(mwdpe:PropertyContainer.OwningPropertyContainer).InlineEditorTemplate}" />
</Grid>
</ControlTemplate>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<mwdpe:PropertyContainer Grid.Row="0" InlineRowTemplate="{StaticResource editorTemplate}"
PropertyEntry="{Binding Path=[Header]}" />
<mwdpe:PropertyContainer Grid.Row="1" InlineRowTemplate="{StaticResource editorTemplate}"
PropertyEntry="{Binding Path=[CommentHeading]}" />
<mwdpe:PropertyContainer Grid.Row="2" InlineRowTemplate="{StaticResource editorTemplate}"
PropertyEntry="{Binding Path=[Comment]}" />
<mwdpe:PropertyContainer Grid.Row="3" InlineRowTemplate="{StaticResource editorTemplate}"
PropertyEntry="{Binding Path=[Value]}" />
</Grid>
</Border>
</DataTemplate>
The PropertyEntry establishes the context for the editors exposed by the PropertyContainer
. Notice that the property name in the Binding Path is surrounded with [ ]
, indicating that this is a indexer.
The InlineRowTemplate
property takes a ControlTemplate
that determines the UI for the property specified by the PropertyEntry
.
Have a look at the DataContext
property for the Grid
. The PropertyContainer OwningPropertyContainer attached property establishes the PropertyContainer
as the DataContext
, giving the UI elements within the Grid
, access to the PropertyContainer
.
In Grid Row 0, the property name is displayed.
In Grid Row 1, the PropertyMarker
and the InlinePropertyEditor
are displayed.
Yes, that’s all the required code to get all the functionality of the PropertyMarker
. Note: This is currently the only way to surface the features exposed by the PropertyMarker
like the Data Binding Builder in your custom editors.
The ContentPresenter
is used to render the UI that edits the property value. In the above ContentPresenter
, I’m using the current InlineEditorTemplate assigned to the property. The first three properties in the category editor all use a TextBox
for editing their values. The last property Value
uses the Rating control. See the next section for how the Rating control is assigned as the InlineEditorTemplate
.
Property Value Editor
The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata to assign the Rating control as the PropertyValueEditor
for the Feedback control’s Value
property.
AddMemberAttributes(feebackType,
Constants.STR_VALUE,
new CategoryAttribute(Constants.STR_CUSTOM),
PropertyValueEditor.CreateEditorAttribute(typeof(RatingSelectorInlineEditor)));
To implement a property value editor, your class must derive from PropertyValueEditor. In the constructor, assign the InlineEditorTemplate
property to a DataTemplate
that has your value editor. In the code I’m using the now familiar Singleton pattern to access the RatingEditorResourceDictionary
’s RatingSelector DataTemplate
.
namespace CiderControls.Common.VisualStudio.Design.Controls {
internal class RatingSelectorInlineEditor : PropertyValueEditor {
public RatingSelectorInlineEditor() {
this.InlineEditorTemplate =
RatingEditorResourceDictionary.Instance.RatingSelector;
}
}
}
The RatingSelector DataTemplate
exposes the Rating control as its UI. Notice the TwoWay
binding mode.
<DataTemplate x:Key="{x:Static c:Constants.STR_RATESELECTORTEMPLATE}">
<cc:Rating Value="{Binding Path=Value, Mode=TwoWay}" />
</DataTemplate>
StringConverter for Properties of Type Object
The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata to enable string editing of the Header
property that is of type object.
Without the TypeConverterAttribute metadata, the Header
property would not be editable in the properties window. Assigning a StringConverter enables the properties window TextBox
string to be assigned to the Header
property value. Without the StringConverter
, the developer will get an error when trying to edit the Header
property value using the properties window.
AddMemberAttributes(feebackType,
Constants.STR_HEADER,
new CategoryAttribute(Constants.STR_CUSTOM),
new TypeConverterAttribute(typeof(StringConverter)));
Category Attribute
Properties are assigned to a category by using a CategoryAttribute as in the below code from CiderControlsAttributeTableBuilder.cs.
You should assign all custom properties to a category so that your properties are displayed in the correct category in the properties window.
AddMemberAttributes(feebackType,
Constants.STR_COMMENTHEADING,
new CategoryAttribute(Constants.STR_CUSTOM));
Description Attribute
While not used in the sample solution, a DescriptionAttribute
can be added to a property’s metadata as in the below code. The description string will appear in the property name ToolTip
when the properties window is in alpha view. This description will also appear in Expression Blend’s property inspector.
AddMemberAttributes(feebackType,
Constants.STR_COMMENT,
new DescriptionAttribute("The comment is filled in by the end user."),
new CategoryAttribute(Constants.STR_CUSTOM));
ToolBoxBrowseable Attribute
The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata to keep the Rating control from appearing in the ToolBox Choose Items dialog.
You should use this metadata to keep controls that you do not want appearing in the ToolBox Choose Items dialog when a developer navigates to your control assembly and selects it or when your control assembly is in a folder that is loaded in the AssemblyFoldersEx
registry key. In the future, I’ll publish a blog post on installing controls into the ToolBox
.
AddTypeAttributes(ratingType,
new ToolboxBrowsableAttribute(false)
);
You can also use the following alternate syntax taking advantage of the static (Shared for VB) No property that returns a new instance of the ToolBoxBrowseableAttribute with Browseable set to false
.
AddTypeAttributes(ratingType,
ToolboxBrowsableAttribute.No
);
Downloads
Links
Close
The title of this post had the words “Code Sharing” in it. The above code provides a design-time for both the WPF & Silverlight controls in the sample solution. I was just thinking how easy it was to forget that this code supports both platforms so easily.
Creating custom design-time experiences for developers using your controls is fun, gives your controls a very professional and polished feel and enables those users to be more productive when creating their applications.
Have a great day!
Just a grain of sand on the worlds beaches.