MonthCalendar Adorner Sample

Co-authored by Jim Nakashima and Timothy Wong.

This post is a follow up to the post I had written showing an example of using adorners.  It will go through an example of writing the design time for an existing MonthCalendar control. 

It covers a few things the last post didn't cover: opening up a ChangeGroup to modify the underlying property and providing undo/redo support, binding between the adorner and the Editing Model, and getting this to work on the June CTP all wrapped up in a more realistic sample.

The Control Being Adorned
First let's have a look at the Calendar control (internal Microsoft sample):

MonthCalendarApp

The actual control we are adorning really isn't important as we are simply changing the FontSize of this control.

The Desired Adorner and Design Time Experience
What we're trying to build is an adorner that is a slider that changes the FontSize of the adorned control relative to the slider position. Here are two screen shots showing different slider placements and the change in the font size:

MonthCalendarDesignTimeFontSizeChangedAdornerExample

Adding the Adorner to the Design Time
We know from my previous post that the first thing we need to do is create an AdornerProvider subclass that will add the slider adorner when the control we are adorning gets selected.  In this sample, that will be the FontSizeSliderAdornerProvider:

class FontSizeSliderAdornerProvider : PrimarySelectionAdornerProvider
{
    . . .
}

This class derives from PrimarySelectionAdornerProvider which means that these adorners will only be activated when the control it adorns is the primary selection.

When the control is selected, the FontSizeSliderAdornerProvider is instantiated and has its Activate() method called. 

An important thing to note here is that since your AdornerProviders are Extensions, they can come and go, which means your Activate() method needs to be able to setup in any context and your Deactivate() method needs to clean everything up.

The initalization of the adorner is shared between the constructor and the Activate() method. 

The constructor will create an AdornerPanel, create and add a slider to it, and add that AdornerPanel to the Adorners collection.  This includes setting up how the adorner lays itself out relative to the control it adorns with all of the AdornerPanel set methods.  How these APIs work is beyond the scope of this post however I will point out the reason there is so many APIs is so that there is flexibility in terms of controlling the adorners position, stretching and scaling as the control resizes.  EventHandlers and data binding are also setup at this point.

public

FontSizeSliderAdornerProvider()
{
    // Add slider control
    _slider = new Slider();
_slider.Minimum = 8;
_slider.Maximum = 24;

    // Layout
    AdornerPanel.SetHorizontalStretch(_slider, AdornerStretch.Stretch);
    AdornerPanel.SetVerticalStretch(_slider, AdornerStretch.None);
    AdornerPanel.SetTargetSizeFactor(_slider, new Vector(1.0, 0));
    AdornerPanel.SetAdornerSizeFactor(_slider, new Vector(0, 1.0));
AdornerPanel.SetAdornerOriginFactor(_slider, new Vector(0, -1.0));
    AdornerPanel.SetAdornerOriginOffset(_slider, new Vector(0, -3));

_myPanel.Children.Add(_slider);

    // handle the value changes of the slider control
    _slider.PreviewMouseLeftButtonUp += new System.Windows.Input.MouseButtonEventHandler(slider_MouseLeftButtonUp);
_slider.PreviewMouseLeftButtonDown += new System.Windows.Input.MouseButtonEventHandler(slider_MouseLeftButtonDown);
_slider.DataContext = this;

    Binding sliderBinding = new Binding("FontSize");
    sliderBinding.Mode = BindingMode.TwoWay;
    _slider.SetBinding(Slider.ValueProperty, sliderBinding);

    Adorners.Add(_myPanel);
}

Activate() will save off the ModelItem which will be used later to modify a property on the adorned control.  It also syncs up the slider to the current value in the ModelItem's FontSize ModelProperty.

protected override void Activate(ModelItem item, UIElement view)
{
    _calendarModelItem = item;
_calendarModelItem.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_calendarModelItem_PropertyChanged);

    FontSize = (double)_calendarModelItem.Properties[AdornedMonthCalendar.FontSizeProperty].Value.GetCurrentValue();
}

Up to here we have created a FontSizeSliderAdornerProvider and we have set it up to display a slider above the control it will adorn with the same width as the control it's adorning when that control becomes the primary selection.  We now need to modify the FontSize property of the adorned control when the slider changes its value.

Modifying the Underlying Property Through the Editing Model
This is done by using a ChangeGroup and the ModelItem we saved off in the Activate() method.  The key thing that needs to be done is this:

    // Make the change on the property
    using (ChangeGroup change = _calendarModelItem.OpenGroup("FontSize change"))
    {
         _calendarModelItem.Properties[AdornedMonthCalendar.FontSizeProperty].SetValue(newValue);
        change.Commit();
    }

For each commited ChangeGroup, there will be a undo/redo item added with the string passed in to OpenGroup().  That is, what occurs in a ChangeGroup will be undoable and redoable with that level of granularity.

If a ChangeGroup is aborted or disposed before Commit() is called, the ChangeGroup will reverse the changes made to the model item.  Additionally, ChangeGroups are global to the designer.  That is, a ChangeGroup can be opened on any item in the designer, not just for the specific item you are modifying.  Finally, they support nesting but must be closed in order.

The ModelItem is used to modify the underlying control's property -- although it is possible to get at the underlying control that will circumvent Cider and is not recommended or supported.

So where do we call the code above to open the change group and modify the property?  Well, we could do it in the Slider's ValueChanged event but that won't provide the kind of granularity we desire.  Instead, we want to open the change group when the MouseLeftButtonDown event occurs and commit that change when the MouseLeftButtonUp event occurs.

void slider_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{

    if (_calendarModelItem != null)
    {
        _batchedChange = _calendarModelItem.OpenGroup("FontSize Change");
    }
}

void slider_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    if (_batchedChange != null)
    {
        _batchedChange.Commit();
        _batchedChange.Dispose();
        _batchedChange = null;
    }
}

Data binding Between the Slider and the Editing Model
So the question now is how the ModelItem FontSize ModelProperty gets updated when the Slider gets updated.  To accomplish this, we want to data bind between the slider and the ModelItem.  ModelItem implements INotifyPropertyChanged but that isn't sufficient since we need to data bind the Slider's value to the FontSize ModelProperty.  To solve this, I implemented INotifyPropertyChanged on the FontSizeSliderAdornerProvider and wrote a FontSize property which updates the ModelItem FontSize ModelProperty:

public double FontSize
{
    get { return _fontSize; }
    set
{
        if (_fontSize == value)
{
             return;
}
        _fontSize = value;
_calendarModelItem.Properties[AdornedMonthCalendar.FontSizeProperty].SetValue(_fontSize);
OnPropertyChanged("FontSize");
}
}

As we saw previously, the Binding is setup in the constructor:

Binding sliderBinding = new Binding("FontSize");
sliderBinding.Mode = BindingMode.TwoWay;
_slider.SetBinding(Slider.ValueProperty, sliderBinding);

And the ModelItem.PropertyChanged is handled to raise a PropertyChanged event on the FontSizeSliderAdornerProvider:

void _calendarModelItem_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    if (String.CompareOrdinal(e.PropertyName, "FontSize") == 0)
{
FontSize = (double)_calendarModelItem.Properties[AdornedMonthCalendar.FontSizeProperty].Value.GetCurrentValue();
}
}

Now, as the Slider is updated, a ChangeGroup is opened and the Editing Model is updated.  Because of the two way data binding, when undo/redo is clicked and the Editing Model changes, the Slider is also updated appropriately.

Tying the AdornerProvider to the Control
What we need to do, is the equivalent of the following:

[Extension(typeof(FontSizeSliderAdornerProvider))]
public class MonthCalendar : Control {. . . }

This won't work for a number of reasons.  The first is that our scenario is that we are creating this design time for an existing control that we can't modify the code... which happens to be true for WPF controls since WPF ships much earlier than Visual Studio Orcas.

The second is that the type FontSizeSliderAdornerProvider derives from PrimarySelectionAdornerProvider which is in Cider's PresentationDesignFramework assembly and ExtensionAttribute is defined in Cider's PresentationDesignCore assembly.  None of Cider's assemblies ship with the WPF redisributable.  That means that on the end user's machine, Cider's assemblies will not be available.

The solution is that Cider will load loosely coupled Metadata Assemblies which will inject design time metadata into the system.  The design time code (i.e. FontSizeSliderAdornerProvider) will reside in a design time assembly which can have references to Cider assemblies.

Since that mechanism isn't yet in place, we will cheat by creating a derived class which declaratively specifies the ExtensionAttribute -- note that this is not suitable solution, simply an interim solution until Metadata Asemblies and the Metadata Store are up and running in Cider.

[Extension(typeof(FontSizeSliderAdornerProvider))]
public class AdornedMonthCalendar : MonthCalendar
{

All that remains now is plopping this AdornedMonthCalendar on a Window loaded in Cider. (a file reference from the WPF Application to this design time assembly is used since project references and in project custom types are not yet supported in Cider).

Making this Sample Work in the June CTP
I've worked around the main limitations with the June CTP - using an ExtensionAttribute directly, using a file reference between the WPF Application and the assembly that contains the adorned control (no project references or in project custom types supported right now) so the main issue remaining is finding the Cider assemblies in the June CTP.

They are installed into the same folder as Visual Studio under a Cider subfolder.  For example, on my machine, they are installed to: C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\Cider.  In this example, the assemblies of interest are PresentationDesignCore.dll (for ExtensionAttribute) and PresentationDesignFramework.dll for all of the Adorner types.