Apr 3, 2009

Property Value Editors for Silverlight Controls

Introduction

This is part of the series on design time implementation changes in Silverlight Toolkit March 2009 Release. This post discusses how to enhance property editing experience for Silverlight controls using property value editor and type converter. I will start with describing the overall property editing architecture in WPF/Silverlight designer extensibility framework, then use examples in  Silverlight Toolkit March 2009 Release to demonstrate how it is done, and unique issues/tricks in Silverlight design time development.

Property Editing Architecture

Visually editing object properties is an important part of designers. Designers usually don’t know how to render properties of custom type (struct, class or interface), much less to provide an nice editing user interface. Control developers usually need to provide TypeConverter, PropertyValueEditor, or both, to provide rendering/editing UI and XAML serialization for properties of custom types.

The designer extensibility framework defines three types of property value editor: inline editor, extended editor, and dialog editor, each implemented by a class:

PropertyValueEditor class diagram

Editing UI of those editors are defined by DataTemplate. The property being edited is exposed to editor as DataContext of PropertyValue type. Editor UI usually bind to the underlying property being edited via one of the three properties of PropertyValue: Value, StringValue, or Collection.

PropertyValueEditor

PropertyValueEditor holds a single inline editor defined by InlineEditorTemplate property. Inline editor appears inside the properties window. Below is an simple example of InlineEditorTemplate that uses a TextBox to display and edit a property:

<DataTemplate x:Key="TextBoxEditor">
   <TextBox Text="{Binding Path=Value}"/>
</DataTemplate>
 

ExtendedPropertyValueEditor

ExtendedPropertyValueEditor has two editors: an inline editor inherited from PropertyValueEditor, and an additional extended editor defined by the ExtendedEditorTemplate property. The extended editor is usually popped up by inline editor via PropertyValueEditorCommands.ShowExtendedPinnedEditor or PropertyValueEditorCommands.ShowExtendedPopupEditor command. Below is a simple example: the inline editor is a button; when clicked, it pops up the extended editor that uses a Slider to display and edit the underlying property.
<DataTemplate x:Key="inlineEditor">
   <Button Content="..." Command="{x:Static PropertyEditing:PropertyValueEditorCommands.ShowDialogEditor}"/>
</DataTemplate>
<DataTemplate x:Key="extendedEditor" xmlns:PropertyEditing="clr-namespace:Microsoft.Windows.Design.PropertyEditing;assembly=Microsoft.Windows.Design.Interaction">
   <Slider x:Name="slider" Value="{Binding Path=Value}" />
</DataTemplate>
 

DialogPropertyValueEditor

DialogPropertyValueEditor has two editors too: the inline editor inherited from PropertyValueEditor, and an additional dialog editor defined by DialogEditorTemplate property. The dialog editor is usually popped up by inline editor via PropertyValueEditorCommands.ShowDialogEditor command. Below is a simple example:

<DataTemplate x:Key="inlineEditor">
   <Button Content="..." Command="{x:Static PropertyEditing:PropertyValueEditorCommands.ShowDialogEditor}"/>
</DataTemplate>
<DataTemplate x:Key="dialogEditor" xmlns:PropertyEditing="clr-namespace:Microsoft.Windows.Design.PropertyEditing;assembly=Microsoft.Windows.Design">
   <Grid>
       <Grid.ColumnDefinitions>
           <ColumnDefinition Width="Auto" />
           <ColumnDefinition Width="*" />
       </Grid.ColumnDefinitions>
       <Grid.RowDefinitions>
           <RowDefinition Height="*"/>
           <RowDefinition Height="*"/>
       </Grid.RowDefinitions>
       <TextBlock Text="User Name:" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,4,4"/>
       <TextBox Text="{Binding Path=Value}" VerticalAlignment="Center" HorizontalAlignment="Stretch" Margin="0,0,4,4" Grid.Column="1"/>
   </Grid>
</DataTemplate>

Implement Custom Property Editor

To implement a custom property editor for a Silverlight control:

  • implement a custom property editor class
  • Associate the custom property editor with a property of a Silverlight control via a AddCustomAttributes call, something like
    attributeTableBuilder.AddCustomAttributes(
       typeof(MyControl),
       "MyProperty",
       new PropertyValueEditor.CreateEditorAttribute(typeof(MyValueEditor)));
    A correctly implemented property value editor must satisfy the following requirements:
  • The property value editor must be designed so that the inline editor and extended editor parts can be used independently.
  • A property value editor must not store state. Property value editors are stateless, might be cached by a host implementation, and can be re-used across multiple property values.
  • A property value editor must not assume that only one value editor part (view/inline/extended) control is active at a given time. For example, a dialog box could have the view part, inline part, and extended UI part active at the same time.
  • A control implemented as part of a property value editor must not store state. A control implemented as part of a value editor should not assume that it will only be bound to one property value. Controls may be recycled to change different property values. Any information that is cached should be flushed if the data model is updated.
  • A control implemented as part of a property value editor must not make any assumptions about the host or its parent controls. The only communication mechanisms that should be used are the PropertyValue data model, by way of the DataContext, and the standard set of commands.
     

Reference Both WPF and Silverlight Assemblies

A design assembly is a .NET/WPF assembly loaded by Visual Studio or Blend, but it usually needs to reference Silverlight assemblies (at least the Silverlight control assembly it provides design time features for). This can create reference ambiguity for design assembly project: it sometimes needs to reference the same fully qualified type in both WPF and Silverlight, say System.Windows.FrameworkElement in both PresentationFramework.dll of WPF and System.Windows.dll of Silverlight. To avoid confusing Visual Studio, you can use extern alias to distinguish WPF and Silverlight references.

For example, see the screenshot below for Controls.DataVisualization.Toolkit.Design project in Silverlight 3 Toolkit in March 2009 Release:

image

  • the project references both PresentationFramework and System.Windows, but System.Windows is under Silverlight alias, instead of the default global alias. The System.Windows reference under Silverlight alias is persisted in .csproj file as below:
    <Reference Include="System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e, processorArchitecture=MSIL">
     <SpecificVersion>False</SpecificVersion>
     <HintPath>..\Binaries\System.Windows.dll</HintPath>
     <Private>False</Private>
     <Aliases>Silverlight</Aliases>
    </Reference>
  • in source code, we add
    extern alias Silverlight;
    using SSW = Silverlight::System.Windows;

    and reference Silverlight’s FrameworkElement via SSW::FrameworkElement.

TextBoxEditor

There is a inline editor TextBoxEditor in Controls.DataVisualization.Toolkit.Design project for displaying and editing Title property of object type for  [Area|Bar|Bubble|Column|Line|Pie|Scatter]Series and Chart controls, as shown below:

TextBoxEditor

Text filled in Title field in Properties window on the right shows up automatically in XAML and design views in the middle.

The implementation of TextBoxEditor followed the steps outlined before:

TextBoxEditor.cs

TextBoxEditor.cs:

// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

using System;
using Microsoft.Windows.Design.PropertyEditing;
using System.Windows;
using System.Windows.Data;

namespace System.Windows.Controls.DataVisualization.Design
{
   /// <summary>
   /// Simple TextBox inline editor.
   /// </summary>
   public partial class TextBoxEditor : PropertyValueEditor
   {
       /// <summary>
       /// Preserve the constructor prototype from PropertyValueEditor.
       /// </summary>
       /// <param name="inlineEditorTemplate">Inline editor template.</param>
       public TextBoxEditor(DataTemplate inlineEditorTemplate)
           : base(inlineEditorTemplate)
       { }

       /// <summary>
       /// Default constructor builds the default TextBox inline editor template.
       /// </summary>
       public TextBoxEditor()
       {
           FrameworkElementFactory textBox = new FrameworkElementFactory(typeof(TextBox));
           Binding binding = new Binding();
           binding.Path = new PropertyPath("Value");
           binding.Mode = BindingMode.TwoWay;
           textBox.SetBinding(TextBox.TextProperty, binding);

           DataTemplate dt = new DataTemplate();
           dt.VisualTree = textBox;

           InlineEditorTemplate = dt;
       }
   }
}

CultureInfoEditor

My colleague RJ wrote inline editor CultureInfoEditor for TimePicker.Culture property, because the default editing experience for CultureInfo in Blend leads to invalid XAML. Below screenshot shows the CultureInfoEditor uses a ComboBox to display all CultureInfo and generates right XAML.

CultureInfoEditor

CultureInfoEditor is a more complex example than TextBoxEditor, really shows how the underlying property is associated with editor via DataContext property.

CultureInfoEditor.cs:

// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

using System.Reflection;
using Microsoft.Windows.Design.PropertyEditing;
using System.Globalization;
using System.ComponentModel;

namespace System.Windows.Controls.Input.Design
{
   /// <summary>
   /// Editor for CultureInfo.
   /// </summary>
   /// <remarks>Currently does not support binding from xaml to the editor.</remarks>
   public class CultureInfoEditor : PropertyValueEditor
   {
       /// <summary>
       /// The ComboBox being used to edit the value.
       /// </summary>
       private ComboBox _owner;

       /// <summary>
       /// Preserve the constructor prototype from PropertyValueEditor.
       /// </summary>
       /// <param name="inlineEditorTemplate">Inline editor template.</param>
       public CultureInfoEditor(DataTemplate inlineEditorTemplate)
           : base(inlineEditorTemplate)
       { }

       /// <summary>
       /// Default constructor builds a ComboBox inline editor template.
       /// </summary>
       public CultureInfoEditor()
       {
           // not using databinding here because Silverlight does not support
           // the WPF CultureConverter that is used by Blend.
           FrameworkElementFactory comboBox = new FrameworkElementFactory(typeof(ComboBox));
           comboBox.AddHandler(
               ComboBox.LoadedEvent,
               new RoutedEventHandler(
                   (sender, e) =>
                   {
                       _owner = (ComboBox) sender;
                       _owner.SelectionChanged += EditorSelectionChanged;
                       INotifyPropertyChanged data = _owner.DataContext as INotifyPropertyChanged;
                       if (data != null)
                       {
                           data.PropertyChanged += DatacontextPropertyChanged;
                       }
                       _owner.DataContextChanged += CultureDatacontextChanged;
                   }));

           comboBox.SetValue(ComboBox.IsEditableProperty, false);
           comboBox.SetValue(ComboBox.DisplayMemberPathProperty, "DisplayName");
           comboBox.SetValue(ComboBox.ItemsSourceProperty, CultureInfo.GetCultures(CultureTypes.SpecificCultures));
           DataTemplate dt = new DataTemplate();
           dt.VisualTree = comboBox;

           InlineEditorTemplate = dt;
       }

       /// <summary>
       /// Handles the SelectionChanged event of the owner control.
       /// </summary>
       /// <param name="sender">The source of the event.</param>
       /// <param name="e">The <see cref="System.Windows.Controls.SelectionChangedEventArgs"/> 
       /// instance containing the event data.</param>
       private void EditorSelectionChanged(object sender, SelectionChangedEventArgs e)
       {
           // serialize with name.
               object DataContext = _owner.DataContext;
               DataContext
                   .GetType()
                   .GetProperty("Value", BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)
                   .SetValue(DataContext, ((CultureInfo)_owner.SelectedItem).Name, new object[] { });
       }

       /// <summary>
       /// Handles the PropertyChanged event of the context object.
       /// </summary>
       /// <param name="sender">The source of the event.</param>
       /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
       private void DatacontextPropertyChanged(object sender, PropertyChangedEventArgs e)
       {
           // deserialize from name.
           if (e.PropertyName == "Value")
           {
               object value = sender
                   .GetType()
                   .GetProperty("Value", BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)
                   .GetValue(sender, new object[] { });

               if (value != null)
               {
                   if (value is string)
                   {
                       CultureInfo setCulture = new CultureInfo(value.ToString());
                       _owner.SelectedItem = setCulture;
                   }
               }
           }
       }

       /// <summary>
       /// Called when the context is changed.
       /// </summary>
       /// <param name="sender">The sender.</param>
       /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
       private void CultureDatacontextChanged(object sender, DependencyPropertyChangedEventArgs e)
       {
           INotifyPropertyChanged old = e.OldValue as INotifyPropertyChanged;
           if (old != null)
           {
               old.PropertyChanged -= DatacontextPropertyChanged;
           }
           INotifyPropertyChanged newDataContext = e.NewValue as INotifyPropertyChanged;
           if (newDataContext != null)
           {
               newDataContext.PropertyChanged += DatacontextPropertyChanged;
           }
       }
   }
}

ExpandableObjectConverter

As discussed at the beginning, besides custom property value editors, sometimes you can use appropriate type converter to provide good editing experience and XAML serialization. One example is ColumnSeries.DependentRangeAxis: it is of IRangeAxis type, Blend doesn’t know how to edit it, so it shows DependentRangeAxis as read only in Properties Panel. By associating ExpandableObjectConverter to ColumnSeries.DependentRangeAxis:

b.AddCustomAttributes(
   Extensions.GetMemberName<ColumnSeries>(x => x.DependentRangeAxis),
   new TypeConverterAttribute(typeof(ExpandableObjectConverter)));
b.AddCustomAttributes(
   Extensions.GetMemberName<ColumnSeries>(x => x.IndependentAxis),
   new TypeConverterAttribute(typeof(ExpandableObjectConverter)));

Blend displays a New button next to this property in Properties Panel; when clicked, it pops up the Select Object dialog, filtered with the property’s type IRangeAxis:

ExpandableObjectConverter

Conclusion

Blend and Silverlight together allow designers to create amazing UI against real controls directly, so it is very important that control developers take designer experience as part of overall control design and implementation. Having a custom property editor that provides nice editing UI and generates correct XAML is an important part of the designer experience. Hopefully this post helps you understand how to create custom property editors. Thanks!