Cider Adorners

Following up on my previous article on Cider Item Creation, I want to talk a bit about Adorners.  Brian Pepin introduces adorners in his article on User Interaction in Cider:

Adorners are WPF elements that float on top of the design surface and track the size and location of the elements they are adorning. All of the UI Cider presents to the user, including snap lines, grab handles and grid rulers, is composed of these adorners. Adorners can contain any control, and an adorner can be associated with a task so they can participate in Cider’s commanding mechanism. Adorners can also get focus and be treated just like normal WPF elements, because that’s what they are.

For example, looking at the following screen shot:

The grid lines, the anchor lines, the grid information, the grab handles...  all are done with adorners. 

One of the cool things is that Cider uses the same infrastructure that we expose as extensibility points for control developers.  This allows us to work with and get validation of the extensibility points we ship to customers.

So what does the code look like to write one of these things? 

I'll go over the high level details around writing a control that has 2 adorners.  Note that if you want to try this at home, you'll have to wait for the next CTP.  I'll be blogging about getting this sample working with the CTP when it becomes available.

In the screen shot below, the Slider is an adorner on the button as is the little square in the upper right hand corner.  The slider adjusts the yellow gradient stop to be the position where the slider is and the little square pops a dialog when clicked.

1) The first step is to decorate your Control such that it knows that there is an attached adorner.

[Extension(typeof(SliderAdornerProvider))]
public class SliderAdornedButton : Button
{
}

The Extension attribute is used to attach the SliderAdorner to the SliderAdornedButton class. 

2) Write the SliderAdornerProvider

The SliderAdornerProvider is an Extension.  So what's an Extension?  Well, essentially, when Cider loads up, it has an ExtensionManager which loads all of the Extensions found in metadata.  This is an extensibility point for adding new functionality into Cider.  They are lightweight features or add ins which don't require much from Cider and are created and destroyed within a given context

Each group of Extensions have an ExtensionServer which manages its Extensions and can request and publish services, listen to events etc.  ExtensionServers exist for the lifetime of Cider and are also a Cider extensibility point. 

In this example, because the SliderAdornerProvider derives from PrimarySelectionAdornerProvider which has an associated ExtensionServer (AdornerProviderExtensionServer), we don't need to write one. 

    class SliderAdornerProvider : PrimarySelectionAdornerProvider
    {
         . . . 
     }

PrimarySelectionAdornerProvider is a Cider provided class that activates the adorner when the control that the adorner is attached to is the primary selection on the design surface.

The bulk of the work is in the constructor which creates two adorners and puts them in the adorner layer.  The first is a Rectangle, the second is a Slider.  The first step is to create the AdornerPanel:

        public SliderAdornerProvider()
        {
            // All adorners are placed in an AdornerPanel
            // for sizing and layout support.
            AdornerPanel myPanel = new AdornerPanel();

A Rectangle is instantiated, its position is set and its added to the AdornerPanel. 

            // 10 pixels Rectangle adorner
            Rectangle rect = new Rectangle();
            rect.Fill = Brushes.Azure;
            rect.Stroke = Brushes.Blue;
            rect.Width = rect.Height = 10.0;
 
            // Inset the slider 5 pixels from the top edge of the
            // control.
            AdornerPanel.SetAdornerOriginOffset(rect, new Vector(5, 5));
            myPanel.Children.Add(rect);

We now add a Task that is fired when the Rectangle adorner is clicked:

            // Add a task to the Rectangle adorner that is
            // fired when we click on it.
            ToolCommand command = new ToolCommand("OpenPopup");
            Task task = new Task();
            task.InputBindings.Add(new InputBinding(command, new ToolGesture(ToolAction.Click, MouseButton.Left)));
            task.ToolCommandBindings.Add(new ToolCommandBinding(command, OnOpenPopup));
            AdornerPanel.SetTask(rect, task);

Essentially, the OnOpenPopup method will be called when the Rectangle adorner is left clicked.

Next up is the Slider Adorner:

           // adding a slider
            Slider slider = new Slider();
            slider.Minimum = 0;
            slider.Maximum = 1;
            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);

Here you'll notice 6 calls to AdornerPanel methods that are setting up the resizing and repositioning behavior of the adorner relative to the control it adorns.  We're still working on these APIs, however the key things to notice are that the Slider adorner will stretch horizontally to always be the same size as the control it adorns, it will not stretch vertically and its positioned above the control it adorns.

Now when the slider values change, I also want to update the control that is being adorned:

            // handle the slide
            slider.ValueChanged += new RoutedPropertyChangedEventHandler<double>(slider_ValueChanged);

Finally, add it to the Adorner layer:

            // Finally, add our panel to the Adorners collection
            Adorners.Add(myPanel);
       }

The event handlers for the adorner events are pretty straight forward (yes, yes it creates a new LinearGradientBrush every time, that can and should be optimized out) :

        void slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            UIElement adornedControl = AdornerPanel.GetAdornedElement((UIElement)sender);
            SliderAdornedButton button = adornedControl as SliderAdornedButton;
            if (button != null)
            {
                LinearGradientBrush gradBrush = new LinearGradientBrush();
                gradBrush.StartPoint = new Point(0, 0);
                gradBrush.EndPoint = new Point(1, 1);
                gradBrush.GradientStops.Add(new GradientStop(Colors.Red, 0.0));
                gradBrush.GradientStops.Add(new GradientStop(Colors.Yellow, e.NewValue));
                gradBrush.GradientStops.Add(new GradientStop(Colors.Green, 1));

                button.Background = gradBrush;
            }
        }

        private void OnOpenPopup(object sender, ExecutedToolEventArgs args)
        {
            MessageBox.Show("Hello Cider");
        }

That's it!  Your first adorner.

One of the things this sample highlights is that adorners can be any class that derives from UIElement.  There is no adorner base class.  It also shows the reuse opportunities for adorners as well as the flexibility they provide in terms of providing a customized design time experience for your control

Obviously there are a lot of details and functionality that was glossed over, I'll be drilling down into various parts of our extensibility mechanisms in the coming weeks.  Stay tuned.