May 11, 2009

Editing Model & Default Initializer for Silverlight Controls

Introduction

This is part of the series on design time feature implementation in Silverlight Toolkit. This post uses the Chart default initializer to illustrate how to implement default initializer for Silverlight controls, and explain the underlying editing model architecture.

Experience

If you install Silverlight 3 Toolkit in March 2009 Release, you can drag the Chart control from Blend Asset Library and drop it onto designer surface, and you have a chart nicely initialized and rendered:

Chart default initializer

You can read more about it in prior post Silverlight Toolkit Design Time Features: March 2009 Release Update.

Architecture

DefaultInitializer

It is actually pretty simple to provide a default initializer for a Silverlight control:

Below screenshot shows the implementation of DefaultInitializer abstract base class, and other classes in Microsoft.Windows.Design.Model namespace that will be discussed shortly:

DefaultInitializer

Initialization is done in InitializeDefaults override, and result is serialized into xaml. So the first step in implementing a default initializer is to define the desired xaml to be produced.

Editing Model

Once the result xaml is defined, I wish there is a higher level abstraction/method that would just take the xaml. But unfortunately we have to use a much lower level (thus more flexible and powerful) imperative API called editing model that consists of classes like ModelItem, ModelProperty, ModelEvent, ModelFactory and ModelService. The MSDN page Editing Model Architecture gives an basic overview:

Your design-time implementation interacts with run-time controls though a programming interface called the editing model. The objects being designed are called editable objects.

Your controls are defined in Extensible Application Markup Language (XAML). You update the XAML for your controls programmatically by using the editing model.

Model, Wrapper, and View

The editing model consists of three functional subunits: a model, a public wrapper that abstracts the model, and a view that represents the user interface (UI) of the model. The model and the view are separate, but the wrapper and model are closely related. The following illustration shows the relationship among the three subunits.

Model, ModelItem, and View relationships

The design environment uses the ModelItem type to communicate with the underlying model. All changes are made to the ModelItem wrappers, which affect the underlying model. This allows the model to be simple. The ModelItem wrappers handle complex designer features, such as transaction support, undo tracking, and change notifications.

Below class diagram may help explain interactions among core classes of the editing model:

Editing Model Class Diagram

Use below xaml as an example:

<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ModelTest.Page" Width="640" Height="480">

<Grid x:Name="LayoutRoot" Background="White">
<Button x:Name="Button">
<Rectangle x:Name="Rectangle" />
</Button>
</Grid>
</UserControl>
  • ModelItem: assume variable item is the ModelItem for the Button control above,
    • Name is persisted as x:Name attribute in xaml: item.Name = “Button” is persisted in xaml as x:Name = “Button”.
    • ItemType is the Type object of the underlying control, and decides the tag used in xaml: item.PropertyType == tyepof(Button), and the xaml tag is <Button />.
    • Properties is the collection of properties, wrapped in ModelProperty type, of the underlying control. For example: item[“ClickMode”] = ClickMode.Release will be persisted in xaml as ClickMode="Release".
    • Content represents the Content property defined in ContentControl (and inherited by its subclasses): item.Content is the ModelItem representing the Rectangle object.
    • Source: when a ModelItem represents an element that’s a property of another element, Source is the ModelProperty wrapper of that property. For example, item.Content.Value.Source == item.Content // the Rectangle.
    • Events represents the collection of events of the underlying control, wrapped in ModelEvent type. From my debugging experience, it is always null, so it seems Events property is not yet supported by Blend3 for Silverlight3 yet.
    • Parent is the the logic parent of the underlying control, wrapped in ModelItem type. ex: item.Parent is the ModelItem for Grid.
    • Root is the ModelItem wrapper for root visual, ex: item.Root represents the UserControl.
  • ModelProperty: assume variable prop is the ModelProperty for Button.Content, i.e. prop = item.Content,
    • Name is the name of the property of the underlying control. It is persisted in xaml as attribute name. ex: prop.Name == “Content”.
    • PropertyType is the Type object of the property of the underlying control this ModelProperty object represents. ex: prop.PropertyType = typeof(object).
    • AttachedOwnerType is the Type object of the control that first defines the property this ModelProperty object represents. ex: prop.AttachedOwnerType == typeof(ContentControl) // note: not Button.
    • Value is the value of the property of the underlying control, wrapped in ModelItem type. ex: prop.Value is the ModelItem representing the Rectangle object.
    • Parent is the control, wrapped in ModelItem type, of which the property belongs to. ex: prop.Parent == item.
  • ModelEvent: as mentioned above, item.Events is always null, so it doesn’t seem that ModelEvent is supported in Blend3 for Silverlight3 yet. But assume it works, and variable evt is the ModelEvent for Button.Click, i.e., evt = item.Events[“Click”],
    • Name is the name of the event. ex: evt.Name == “Click”.
    • EventType is the type of the event delegate, ex: evt.EventType == typeof(RoutedEventHandler).
    • Handlers is the string collection of event handlers, ex: evt.Handlers = new string[] { “Button_Click” } will be persisted as Click = “Button_Click”. Note: this is pure speculation, since ModelEvent isn’t yet supported, and I don’t know whether/when/how it will be supported for Silverlight.
    • Parent is the control, wrapped in ModelItem type, of which this event belongs to. ex: evt.Parent == item.

Implementation

The Chart default initializer class ChartDefaultInitializer is implemented in ChartDefaultInitializer.cs, and registered in ChartMetadata.cs. Both files are in Controls.DataVisualization.Toolkit.Design.csproj of Silverlight.Controls.Design.sln. Please read prior post Design Time Feature Implementation in Silverlight Toolkit for more information.

Result XAML

As the first step, define the result xaml to be produced by ChartDefaultInitializer:

   1: <Charting:Chart Title="Chart Title">
   2:     <Charting:Chart.DataContext>
   3:         <PointCollection>
   4:             <Point X="1" Y="10" />
   5:             <Point X="2" Y="20" />
   6:             <Point X="3" Y="30" />
   7:             <Point X="4" Y="40" />
   8:         </PointCollection>
   9:         <Charting:Chart.Series>
  10:             <Charting:ColumnSeries ItemsSource="{Binding}"
  11:                 DependentValuePath="X"
  12:                 IndependentValuePath="Y" />
  13:         </Charting:Chart.Series>
  14:     </Charting:Chart.DataContext>
  15: </Charting:Chart>

Registration

Done in ChartMetadata.cs with following code:

b.AddCustomAttributes(new FeatureAttribute(typeof(ChartDefaultInitializer)));

References

See below screenshot:

References

Even though DefaultInitializer is defined in version 3.5 of Microsoft.Windows.Design.Interaction.dll (under %devenvdir%\PublicAssemblies, i.e. c:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PublicAssemblies on 32bit Windows), it is not supported by Blend2 or Visual Studio 2008 for Silverlight. It is supported by Blend3 and Visual Studio 2010, but they both switch to newer version of Microsoft.Windows.Design*.dll (3.7 for Blend 3 Preview, 4.0 for Blend 3 RTM and Visual Studio 2010), so we need to link against the newer version of MWDs. Please see prior post How to Write Silverlight Design Time for All Designers: Visual Studio 2008, Blend 2; Blend 3, and Visual Studio 2010 for more information.

Please also notice that reference to Silverlight’s System.Windows.dll is aliased, and the PointCollection is specifically called out to be the Silverlight one, not WPF’s. Since WPF and Silverlight share a lot of classes in same namespaces, it is important that Silverlight types, not WPF’s, are used in creating ModelItem and ModelProperty in default initializer for Silverlight controls.

Collections

Please note in below screenshot that while all other property value can be set via ModelProperty.SetValue:

ChartDefaultInitializer

Chart.Series is of collection type (Collection<Series>), so its value has to be set by first creating a ModelItem via ModelFactory.CreateItem for the value, and then add the created ModelItem via ModelProperty.Collection.Add. Otherwise, even though the correct xaml may be generated, Blend won’t refresh to render the Chart control correctly, you have to reload the page to have it rendered correctly.

ChartDefaultInitializer.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.

extern alias Silverlight;
using System.Windows.Controls.DataVisualization.Charting;
using System.Windows.Controls.Design.Common;
using Microsoft.Windows.Design.Metadata;
using Microsoft.Windows.Design.Model;
using SSW = Silverlight::System.Windows;
using SSWD = Silverlight::System.Windows.Data;
using SSWM = Silverlight::System.Windows.Media;

namespace System.Windows.Controls.DataVisualization.Design
{
/// <summary>
/// Default initializer for chart.
/// </summary>
internal class ChartDefaultInitializer : DefaultInitializer
{
/// <summary>
/// Sets the default property values for chart.
/// </summary>
/// <param name="item">Chart ModelItem.</param>
public override void InitializeDefaults(ModelItem item)
{
string propertyName;

// <Charting:Chart Title="Chart Title">
propertyName = Extensions.GetMemberName<Chart>(x => x.Title);
item.Properties[propertyName].SetValue(Properties.Resources.ChartTitle);

// <Charting:Chart.DataContext>
// <PointCollection>
// <Point X="1" Y="10" />
// <Point X="2" Y="20" />
// <Point X="3" Y="30" />
// <Point X="4" Y="40" />
// </PointCollection>
// </Charting:Chart.DataContext>

SSWM::PointCollection defaultItemsSource = new SSWM::PointCollection();
for (int i = 1; i <= 4; i++)
{
defaultItemsSource.Add(new SSW::Point(i, 10 * i));
}

propertyName = Extensions.GetMemberName<Chart>(x => x.DataContext);
item.Properties[propertyName].SetValue(defaultItemsSource);

// <Charting:Chart.Series>
// <Charting:ColumnSeries ItemsSource="{Binding}"
// DependentValuePath="X"
// IndependentValuePath="Y" />
// </Charting:Chart.Series>

ModelItem columnSeries = ModelFactory.CreateItem(item.Context, typeof(ColumnSeries));
propertyName = Extensions.GetMemberName<ColumnSeries>(x => x.ItemsSource);
columnSeries.Properties[propertyName].SetValue(ModelFactory.CreateItem(columnSeries.Context, typeof(SSWD::Binding)));
propertyName = Extensions.GetMemberName<ColumnSeries>(x => x.DependentValuePath);
columnSeries.Properties[propertyName].SetValue("X");
propertyName = Extensions.GetMemberName<ColumnSeries>(x => x.IndependentValuePath);
columnSeries.Properties[propertyName].SetValue("Y");

propertyName = Extensions.GetMemberName<Chart>(x => x.Series);
item.Properties[propertyName].Collection.Add(columnSeries);
}
}
}