Поделиться через


WPF & Silverlight Design-Time Code Sharing – Part II

In WPF & Silverlight Design-Time Code Sharing - Part I, I introduced the 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 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.

To follow along, open the sample solution and view the CiderControlsAttributeTableBuilder.cs or CiderControlsAttributeTableBuilder.vb file in the CiderControls.Common.VisualStudio.Design project.

Table of Contents

  1. Metadata Loading Revisited
  2. Control Default Initializer
  3. Control Context Menu
  4. Control Adorner
  5. Category Editor
  6. Property Value Editor
  7. StringConverter for Properties of Type Object
  8. Category Attribute
  9. Description Attribute
  10. ToolBoxBrowseable Attribute
  11. Downloads
  12. Links
  13. Close
  14. Comments

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-passes 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 {

  //TODO 14 - FeedbackControlInitializer

  /// <summary>
  /// An initializer is called during control creation on the design surface
  /// You can set properties on the ModelItem to adjust the default values.
  /// </summary>
  internal class FeedbackControlInitializer : DefaultInitializer {

    public FeedbackControlInitializer() {
    }

    /// <summary>
    /// Callback when the designer has created the FeedbackControl.
    /// </summary>
    public override void InitializeDefaults(ModelItem item) {
      base.InitializeDefaults(item);
      // The below values are set demonstration purposes to show how its done.
      // See how nice the platform neutral PropertyIdentifiers work.  
      // ex: MyPlatformTypes.Feedback.CornerRadiusProperty
      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 

MenuAction

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);

  //MenuGroup enables having submenus
  _feedbackGroup = new MenuGroup(Constants.STR_FEEDBACKGROUP, Strings.MenuFeedbackSetValue);
  _feedbackGroup.HasDropDown = true;

  // Add the 6 MenuActions
  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 UIElements 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.  

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 AdornderProvider.  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 {

  /// <summary>
  /// The main kinds of tools are:
  ///   - CreationTool (when the toolbox is activated - and clicking means create new object)
  ///   - SelectionTool (when clicking means select object)
  ///   
  /// For this adorner, we don't want it to be in the way of creating new objects 
  /// so we'll hide it when the current tool is not the SelectionTool. 
  /// </summary>
  /// <param name="tool"></param>
  /// <returns></returns>
  public override bool IsToolSupported(Tool tool) {
    if (tool is SelectionTool) {
      return true;
    }
    return false;
  }

  /// <summary>
  /// Activate is called when the policy (specified by the UsesItemPolicy attribute)
  /// for the adorner provider says it's found a new model item.  
  /// </summary>
  protected override void Activate(ModelItem item) {

    // Create the adorner
    // Add required event handlers

    base.Activate(item);
  }

  /// <summary>
  /// Deactivate is called when the policy says a previously active item is no 
  /// longer within the policy. 
  /// </summary>
  protected override void Deactivate() {

    // unhook event handlers added in the Active method.
    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 {

  //TODO 11 - FeedbackControlSelectionPolicy

  /// <summary>
  /// This FeedbackControlSelectionPolicy provides for hiding of the adorner when the
  /// Feedback control is being resized or moved. 
  /// </summary>
  internal class FeedbackControlSelectionPolicy : PrimarySelectionPolicy {

    private bool _isFocused;

    public FeedbackControlSelectionPolicy() {
    }

    /// <summary>
    /// Determines if an item is part of the policy.  Called back from 
    /// OnPolicyItemsChanged on all the items in e.ItemsAdded 
    /// </summary>
    /// <param name="selection"></param>
    /// <param name="item"></param>
    /// <returns></returns>
    protected override bool IsInPolicy(Selection selection, ModelItem item) {
      bool inPolicy = base.IsInPolicy(selection, item);
      return inPolicy && !_isFocused;
    }

    /// <summary>
    /// Called when this policy is activated.  In order to determine 
    /// which items are in/out of a policy, we need to subscribe to events.
    /// The base class, PrimarySelectionPolicy, subscribes to 
    /// changes in selection.  Additionally we need to subscribe to changes
    /// in the focused task so we can disable during resize/move.
    /// </summary>
    protected override void OnActivated() {
      this.Context.Items.Subscribe<FocusedTask>(OnFocusedTaskChanged);
      base.OnActivated();
    }

    /// <summary>
    /// When the policy is no longer in use, we unsubscribe from the events we hooked up to 
    /// in Activated.
    /// </summary>        
    protected override void OnDeactivated() {
      this.Context.Items.Unsubscribe<FocusedTask>(OnFocusedTaskChanged);
      base.OnDeactivated();
    }

    // If the Feedback control is selected and there is a FocusedTask like moving or 
    //   resizing the control, the Feedback control will be added to the Removed 
    //   parameter and the adorner will be removed from the Feeback control.
    // During the drag or resize operation this code will be bypassed on repeated calls until
    //   the move or resize is completed.

    // If the Feedback control is selected and there is no FocusedTask like moving or being 
    //   resized, the Feedback control will be added to the Added parameter and the adorner
    //   will be shown on the Feedback control.

    /// <summary>
    /// Tasks are operations on tools, for example, the drag resize task is an operation
    /// on the SelectionTool, and the click draw task is an operation on the CreationTool.
    /// 
    /// When a focused task is active we want to hide the ratings control because
    /// we don't know what the interaction will be.
    /// </summary>
    /// <param name="f"></param>
    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.

CategoryView

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 TextBoxes for strings or numbers, CheckBoxes for Booleans and ComboBoxes 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 {

  /// <summary>
  /// A category editor is a group of properties that can be edited together.
  /// 
  /// The Text and Brushes category editors are examples of a category editor.
  /// The Text category editor consumes all the Text related properties and 
  /// displays them in a complex UI.  The brush category editor consumes all the 
  /// properties of type brush, displays UI and a list of properties to manipulate.
  /// </summary>
  internal class FeedbackControlCategoryEditor : CategoryEditor {

    public FeedbackControlCategoryEditor() {
    }

    /// <summary>
    /// Provides ability to list or not list properties by name in a category.
    /// </summary>
    /// <param name="propertyEntry"></param>
    /// <returns>return true if the property is edited by this category editor.
    /// Returning false will cause the property to be listed as a separate row
    /// within the category.</returns>
    public override bool ConsumesProperty(PropertyEntry propertyEntry) {
      return true;
    }

    /// <summary>
    /// Caegory editor data template
    /// </summary>
    public override DataTemplate EditorTemplate {
      get {
        return FeedbackControlResourceDictionary.Instance.FeedbackCategoryEditor;
      }
    }

    /// <summary>
    /// Used by Blend only
    /// </summary>
    /// <param name="desiredSize"></param>
    /// <returns></returns>
    public override object GetImage(Size desiredSize) {
      return null;
    }

    /// <summary>
    /// Defines the category this category editor is for.
    /// </summary>
    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 DataTemplates 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 your code by removing quoted strings, while IntelliSense provides quick access to your defined constants when editing code.

 namespace CiderControls.Common.VisualStudio.Design.Controls {

  /// <summary>
  /// The FeedBackCategoryEditor property supplies a strong typed access 
  /// to the FeedBackCategoryEditor DataTemplate located in the 
  /// FeedbackControlResourceDictionary.xaml file.
  ///    
  /// This class implements the Singleton pattern so that the resource 
  /// dictionary is only created once and is accessed through the Instance property.
  /// </summary>
  internal partial class FeedbackControlResourceDictionary : ResourceDictionary {

    [ThreadStatic]
    private static FeedbackControlResourceDictionary _instance;

    private FeedbackControlResourceDictionary() {
      InitializeComponent();
    }

    /// <summary>
    /// return a cached copy of the resource dictionary
    /// </summary>
    internal static FeedbackControlResourceDictionary Instance {
      get {
        if (_instance == null) {
          _instance = new FeedbackControlResourceDictionary();
        }
        return _instance;
      }
    }

    /// <summary>
    /// DataTemplate for the FeedbackCategoryEditor
    /// </summary>
    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. Noticed 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 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 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 {

  /// <summary>
  /// Rating control property value editor
  /// </summary>
  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.

 <!-- Using the Rating Control for the Value property inline editor. -->
<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 

C# Source Code Download

VB.NET Source Code Download

PowerPoint Slides from the Development Tools Ecosystem Summit Presentation

Links 

Walkthrough: Creating a Category Editor

Walkthrough: Implementing an Inline Value Editor

How to: Create a Value Editor

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.

Comments 

Microsoft values your opinion about our products and documentation. In addition to your general feedback it is very helpful to understand:

  • How the above feature enables your workflow
  • What is missing from the above feature that would be helpful to you

Thank you for your feedback and have a great day,

Karl Shifflett

Visual Studio Cider Team