Walkthrough: Implementing a Rail Inside a Control

This walkthrough shows how to create a rail adorner for a Windows Presentation Foundation (WPF) custom control. A rail adorner adds a marker or rule along one side of a control that is non-scaling along a single axis. The adorner is a slider, which is placed within the control. You can use this adorner to set the RenderTransform property on a custom button control. Setting the RenderTransform property skews the control. For a complete code listing, see How to: Implement a Rail Inside a Control.

In this walkthrough, you perform the following tasks:

  • Create a WPF custom control library project.

  • Create a separate assembly for design-time metadata.

  • Implement the adorner provider.

  • Test the control at design time.

When you are finished, you will know how create an adorner for a custom control.

Note

The dialog boxes and menu commands you see might differ from those described in Help depending on your active settings or edition. To change your settings, choose Import and Export Settings on the Tools menu. For more information, see Visual Studio Settings.

Prerequisites

You need the following components to complete this walkthrough:

  • Visual Studio 2008.

Creating the Custom Control

The first step is to create the project for the custom control. The control is a simple button with small amount of design-time code, which uses the GetIsInDesignMode method to implement a design-time behavior.

To create the custom control

  1. Create a new WPF Custom Control Library project in Visual Basic or Visual C# named SkewButtonLibrary.

    The code for CustomControl1 opens in the Code Editor.

  2. In Solution Explorer, change the name of the code file to SkewButton.cs or SkewButton.vb. If a message box appears that asks if you want to perform a rename for all references in this project, click Yes.

  3. In Solution Explorer, expand the Themes folder.

  4. Double-click Generic.xaml.

    Generic.xaml opens in the WPF Designer.

  5. In XAML view, replace all occurrences of "CustomControl1" with "SkewButton".

  6. Open SkewButton.cs or SkewButton.vb in the Code Editor.

  7. Replace the automatically generated code with the following code. The SkewButton custom control inherits from Button and displays the text "Design mode active" when the button appears in the designer. The GetIsInDesignMode check and the following design-time code are optional and shown only for demonstration.

    Imports System
    Imports System.Collections.Generic
    Imports System.Text
    Imports System.Windows.Controls
    Imports System.Windows.Media
    Imports System.ComponentModel
    
    
    ' The SkewButton control implements a button 
    ' with a skew transformation applied. 
    Public Class SkewButton
        Inherits Button
    
        Public Sub New() 
    
            ' The following code enables custom design-mode logic. 
            ' The GetIsInDesignMode check and the following design-time  
            ' code are optional and shown only for demonstration. 
            If DesignerProperties.GetIsInDesignMode(Me) Then
                Content = "Design mode active" 
            End If 
    
        End Sub 
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows.Controls;
    using System.Windows.Media;
    using System.ComponentModel;
    
    namespace SkewButtonLibrary
    {
        // The SkewButton control implements a button 
        // with a skew transformation applied. 
        public class SkewButton : Button
        {
            public SkewButton()
            {   
                // The following code enables custom design-mode logic. 
                // The GetIsInDesignMode check and the following design-time  
                // code are optional and shown only for demonstration. 
                if (DesignerProperties.GetIsInDesignMode(this))
                {
                    Content = "Design mode active";
                }
            }
        }
    }
    
  8. Set the project's output path to "bin\".

  9. Build the solution.

Creating the Design-time Metadata Assembly

Design-time code is deployed in special metadata assemblies. For this walkthrough, the custom adorner is deployed in an assembly named SkewButtonLibrary.VisualStudio.Design. For more information, see Metadata Store.

To create the design-time metadata assembly

  1. Add a new Class Library project in Visual Basic or Visual C# named SkewButtonLibrary.VisualStudio.Design to the solution.

  2. Set the project's output path to "..\SkewButtonLibrary\bin\". This keeps the control's assembly and the metadata assembly in the same folder, which enables metadata discovery for designers.

  3. Add references to the following WPF assemblies.

    • PresentationCore

    • PresentationFramework

    • WindowsBase

  4. Add references to the following WPF Designer assemblies.

    • Microsoft.Windows.Design

    • Microsoft.Windows.Design.Extensibility

    • Microsoft.Windows.Design.Interaction

  5. Add a reference to the SkewButtonLibrary project.

  6. In Solution Explorer, change the name of the Class1 code file to Metadata.cs or Metadata.vb. If a message box appears that asks if you want to perform a rename for all references in this project, click Yes.

  7. Replace the automatically generated code with the following code. This code creates an AttributeTable which attaches the custom design-time implementation to the SkewButton class.

    Imports System
    Imports System.Collections.Generic
    Imports System.Text
    Imports System.ComponentModel
    Imports System.Windows.Media
    Imports System.Windows.Controls
    Imports System.Windows
    
    Imports Microsoft.Windows.Design.Features
    Imports Microsoft.Windows.Design.Metadata
    Imports SkewButtonLibrary.VisualStudio.Design
    
    Imports SkewButtonLibrary
    
    ' Container for any general design-time metadata to initialize. 
    ' Designers look for a type in the design-time assembly that  
    ' implements IRegisterMetadata. If found, designers instantiate  
    ' this class and call its Register() method automatically. 
    Friend Class Metadata
        Implements IRegisterMetadata
    
        ' Called by the designer to register any design-time metadata. 
        Public Sub Register() Implements IRegisterMetadata.Register
            Dim builder As New AttributeTableBuilder()
    
            ' Add the adorner provider to the design-time metadata.
            builder.AddCustomAttributes(GetType(SkewButton), New FeatureAttribute(GetType(SkewButtonAdornerProvider)))
    
            MetadataStore.AddAttributeTable(builder.CreateTable())
    
        End Sub 
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.ComponentModel;
    using System.Windows.Media;
    using System.Windows.Controls;
    using System.Windows;
    
    using SkewButtonLibrary;
    using Microsoft.Windows.Design.Features;
    using Microsoft.Windows.Design.Metadata;
    using SkewButtonLibrary.VisualStudio.Design;
    
    namespace CiderPropertiesTester
    {
        // Container for any general design-time metadata to initialize. 
        // Designers look for a type in the design-time assembly that  
        // implements IRegisterMetadata. If found, designers instantiate  
        // this class and call its Register() method automatically. 
        internal class Metadata : IRegisterMetadata
        {
            // Called by the designer to register any design-time metadata. 
            public void Register()
            {
                AttributeTableBuilder builder = new AttributeTableBuilder();
    
                // Add the adorner provider to the design-time metadata.
                builder.AddCustomAttributes(
                    typeof(SkewButton), 
                    new FeatureAttribute(typeof(SkewButtonAdornerProvider)));
    
                MetadataStore.AddAttributeTable(builder.CreateTable());
            }
        }
    }
    
  8. Save the solution.

Implementing the Adorner Provider

The adorner provider is implemented in a type named SkewButtonAdornerProvider. This adorner FeatureProvider enables setting the control's RenderTransform property at design time.

To implement the adorner provider

  1. Add a new class named SkewButtonAdornerProvider to the SkewButtonLibrary.VisualStudio.Design project.

  2. In the Code Editor for SkewButtonAdornerProvider, replace the automatically generated code with the following code. This code implements a PrimarySelectionAdornerProvider which provides a custom adorner.

    Imports System
    Imports System.Collections.Generic
    Imports System.Text
    Imports System.Windows.Input
    Imports System.Windows
    Imports System.Windows.Automation
    Imports System.Windows.Controls
    Imports System.Windows.Media
    Imports System.Windows.Shapes
    Imports Microsoft.Windows.Design.Interaction
    Imports Microsoft.Windows.Design.Model
    
    
    ' The following class implements an adorner provider for the  
    ' SkewButton control. The adorner is a slider control, which  
    ' changes the SkewTransform of the SkewButton along the x-axis.  
    ' The adorner is placed inside the adorned control. 
    Class SkewButtonAdornerProvider
        Inherits PrimarySelectionAdornerProvider
        Private adornedControlModel As ModelItem
        Private batchedChange As ModelEditingScope
        Private skewSlider As Slider
        Private skewSliderAdornerPanel As AdornerPanel
    
        Public Sub New() 
            skewSlider = New Slider()
        End Sub 
    
        ' The following method is called when the adorner is activated. 
        ' It creates the adorner control, sets up the adorner panel, 
        ' and attaches a ModelItem to the SkewButton. 
        Protected Overrides Sub Activate(ByVal item As ModelItem, ByVal view As DependencyObject) 
            ' Save the ModelItem and hook into when it changes. 
            ' This enables updating the slider position when  
            ' a new background value is set.
            adornedControlModel = item
            AddHandler adornedControlModel.PropertyChanged, AddressOf AdornedControlModel_PropertyChanged
    
            ' Setup the slider's min and max values.
            skewSlider.Minimum = 0
            skewSlider.Maximum = 45
    
            ' Set the slider's background to the rail fill color.
            skewSlider.Background = AdornerColors.RailFillBrush
    
            ' All adorners are placed in an AdornerPanel 
            ' for sizing and layout support. 
            Dim panel As AdornerPanel = Me.Panel
    
            ' Place the slider in the adorner panel. 
            Dim placement As New AdornerPlacementCollection()
            AdornerPanel.SetHorizontalStretch(skewSlider, AdornerStretch.Stretch)
            AdornerPanel.SetVerticalStretch(skewSlider, AdornerStretch.None)
    
            ' Use layout space for the adorner panel. 
            ' If render space were used, the slider would skew along with the button.
            panel.CoordinateSpace = AdornerCoordinateSpaces.Layout
    
            ' The adorner's width is relative to the content. 
            ' The slider extends half the width of the control it adorns.
            placement.SizeRelativeToContentWidth(0.5, 0)
    
            ' Center the adorner within the control.
            placement.PositionRelativeToAdornerWidth(0.5, 0)
    
            ' The adorner's height is the same as the slider's.
            placement.SizeRelativeToAdornerDesiredHeight(1.0, 0)
    
            ' Position the adorner within the control it adorns.
            placement.PositionRelativeToAdornerHeight(1.0, 0)
    
            AdornerPanel.SetPlacements(skewSlider, placement)
    
            ' Initialize the slider when it is loaded. 
            AddHandler skewSlider.Loaded, AddressOf slider_Loaded
    
            ' Handle the value changes of the slider control. 
            AddHandler skewSlider.ValueChanged, AddressOf slider_ValueChanged
    
            AddHandler skewSlider.PreviewMouseLeftButtonUp, AddressOf slider_MouseLeftButtonUp
    
            AddHandler skewSlider.PreviewMouseLeftButtonDown, AddressOf slider_MouseLeftButtonDown
    
            ' Run the base implementation. 
            MyBase.Activate(item, view)
    
        End Sub 
    
        ' The Panel utility property demand-creates the  
        ' adorner panel and adds it to the provider's  
        ' Adorners collection. 
        Public ReadOnly Property Panel() As AdornerPanel
            Get 
                If Me.skewSliderAdornerPanel Is Nothing Then 
                    Me.skewSliderAdornerPanel = New AdornerPanel()
    
                    ' Add the adorner to the adorner panel. 
                    Me.skewSliderAdornerPanel.Children.Add(skewSlider)
    
                    ' Add the panel to the Adorners collection.
                    Adorners.Add(skewSliderAdornerPanel)
                End If 
    
                Return Me.skewSliderAdornerPanel
            End Get 
        End Property 
    
        ' The following method deactivates the adorner. 
        Protected Overrides Sub Deactivate() 
    
            RemoveHandler adornedControlModel.PropertyChanged, AddressOf AdornedControlModel_PropertyChanged
    
            MyBase.Deactivate()
    
        End Sub 
    
    
        ' The following method handles the Loaded event. 
        ' It assigns the slider control's initial value. 
        Sub slider_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs) 
            skewSlider.Value = GetCurrentSkewAngle()
    
        End Sub 
    
    
        ' The following method handles the PropertyChanged event. 
        ' It updates the slider control's value tf the SkewButton control's  
        ' RenderTransform property changed, 
        Sub AdornedControlModel_PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) 
            If e.PropertyName = "RenderTransform" Then 
                ' Assign the SkewButton control's skew angle to the slider.
                skewSlider.Value = GetCurrentSkewAngle()
            End If 
    
        End Sub 
    
    
        ' The following method handles the MouseLeftButtonDown event. 
        ' It calls the BeginEdit method on the ModelItem which represents the 
        ' Skewcontrol. 
        Sub slider_MouseLeftButtonDown(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) 
            batchedChange = adornedControlModel.BeginEdit()
    
        End Sub 
    
    
        ' The following method handles the MouseLeftButtonUp event. 
        ' It commits any changes made to the ModelItem which represents the 
        ' Skewcontrol. 
        Sub slider_MouseLeftButtonUp(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) 
            If Not (batchedChange Is Nothing) Then
                batchedChange.Complete()
                batchedChange.Dispose()
                batchedChange = Nothing 
            End If 
    
        End Sub 
    
    
        ' The following method handles the slider control's  
        ' ValueChanged event. It sets the value of the  
        ' RenderTransform property by using the ModelProperty 
        ' type. 
        Sub slider_ValueChanged(ByVal sender As Object, ByVal e As RoutedPropertyChangedEventArgs(Of Double))
    
            Dim newSkewValue As Double = e.NewValue
    
            ' During setup, don't make a value local and set the skew angle. 
            If newSkewValue = GetCurrentSkewAngle() Then 
                Return 
            End If 
    
            ' Access the SkewButton control's RenderTransform property 
            ' by using the ModelProperty type. 
            Dim skewProperty As ModelProperty = adornedControlModel.Properties(Control.RenderTransformProperty)
    
            If Not skewProperty.IsSet Then 
                ' If the value isn't local, make it local  
                ' before setting a sub-property value.
                skewProperty.SetValue(skewProperty.ComputedValue)
            End If 
    
            ' Set the RenderTransform property on the SkewButton.
            skewProperty.SetValue(New SkewTransform(newSkewValue, 0))
    
        End Sub 
    
    
        ' This utility method gets the SkewControl control's 
        ' skew angle by using the ModelItem. 
        Private Function GetCurrentSkewAngle()
            Dim skewXform As SkewTransform = adornedControlModel.Properties(Control.RenderTransformProperty).ComputedValue
            Return skewXform.AngleX
        End Function 
    
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows.Input;
    using System.Windows;
    using System.Windows.Automation;
    using System.Windows.Controls;
    using System.Windows.Media;
    using System.Windows.Shapes;
    using Microsoft.Windows.Design.Interaction;
    using Microsoft.Windows.Design.Model;
    
    namespace SkewButtonLibrary.VisualStudio.Design
    {
        // The following class implements an adorner provider for the  
        // SkewButton control. The adorner is a slider control, which  
        // changes the SkewTransform of the SkewButton along the x-axis.  
        // The adorner is placed inside the adorned control. 
        class SkewButtonAdornerProvider : PrimarySelectionAdornerProvider
        {
            private ModelItem adornedControlModel;
            private ModelEditingScope batchedChange;
            private Slider skewSlider;
            private AdornerPanel skewButtonAdornerPanel;
    
            public SkewButtonAdornerProvider()
            {
                skewSlider = new Slider();
            }
    
            // The following method is called when the adorner is activated. 
            // It creates the adorner control, sets up the adorner panel, 
            // and attaches a ModelItem to the SkewButton. 
            protected override void Activate(ModelItem item, DependencyObject view)
            {
                // Save the ModelItem and hook into when it changes. 
                // This enables updating the slider position when  
                // a new background value is set.
                adornedControlModel = item;
                adornedControlModel.PropertyChanged += 
                    new System.ComponentModel.PropertyChangedEventHandler(
                        AdornedControlModel_PropertyChanged);
    
                // Setup the slider's min and max values.
                skewSlider.Minimum = 0;
                skewSlider.Maximum = 45;
    
                // Set the slider's background to the rail fill color.
                skewSlider.Background = AdornerColors.RailFillBrush;
    
                // All adorners are placed in an AdornerPanel 
                // for sizing and layout support.
                AdornerPanel panel = this.Panel;
    
                // Place the slider in the adorner panel.
                AdornerPlacementCollection placement = new AdornerPlacementCollection();
                AdornerPanel.SetHorizontalStretch(skewSlider, AdornerStretch.Stretch);
                AdornerPanel.SetVerticalStretch(skewSlider, AdornerStretch.None);
    
                // Use layout space for the adorner panel. 
                // If render space were used, the slider would skew along with the button.
                panel.CoordinateSpace = AdornerCoordinateSpaces.Layout;
    
                // The adorner's width is relative to the content. 
                // The slider extends half the width of the control it adorns.
                placement.SizeRelativeToContentWidth(0.5, 0);
    
                // Center the adorner within the control.
                placement.PositionRelativeToAdornerWidth(0.5, 0);
    
                // The adorner's height is the same as the slider's.
                placement.SizeRelativeToAdornerDesiredHeight(1.0, 0);
    
                // Position the adorner within the control it adorns.
                placement.PositionRelativeToAdornerHeight(1.0, 0);
    
                AdornerPanel.SetPlacements(skewSlider, placement);
    
                // Initialize the slider when it is loaded.
                skewSlider.Loaded += new RoutedEventHandler(slider_Loaded);
    
                // Handle the value changes of the slider control.
                skewSlider.ValueChanged += 
                    new RoutedPropertyChangedEventHandler<double>(
                        slider_ValueChanged);
    
                skewSlider.PreviewMouseLeftButtonUp += 
                    new System.Windows.Input.MouseButtonEventHandler(
                        slider_MouseLeftButtonUp);
    
                skewSlider.PreviewMouseLeftButtonDown += 
                    new System.Windows.Input.MouseButtonEventHandler(
                        slider_MouseLeftButtonDown);
    
                // Run the base implementation. 
                base.Activate(item, view);
            }
    
            // The Panel utility property demand-creates the  
            // adorner panel and adds it to the provider's  
            // Adorners collection. 
            public AdornerPanel Panel
            {
                get
                {
                    if (this.skewButtonAdornerPanel == null)
                    {
                        skewButtonAdornerPanel = new AdornerPanel();
    
                        skewButtonAdornerPanel.Children.Add(skewSlider);
    
                        // Add the panel to the Adorners collection.
                        Adorners.Add(skewButtonAdornerPanel);
                    }
    
                    return this.skewButtonAdornerPanel;
                }
            }
    
            // The following method deactivates the adorner. 
            protected override void Deactivate()
            {
                adornedControlModel.PropertyChanged -= 
                    new System.ComponentModel.PropertyChangedEventHandler(
                        AdornedControlModel_PropertyChanged);
    
                base.Deactivate();
            }
    
            // The following method handles the Loaded event. 
            // It assigns the slider control's initial value. 
            void slider_Loaded(object sender, RoutedEventArgs e)
            {   
                skewSlider.Value = GetCurrentSkewAngle();
            }
    
            // The following method handles the PropertyChanged event. 
            // It updates the slider control's value tf the SkewButton control's  
            // RenderTransform property changed, 
            void AdornedControlModel_PropertyChanged(
                object sender, 
                System.ComponentModel.PropertyChangedEventArgs e)
            {
                if (e.PropertyName == "RenderTransform")
                {
                    // Assign the SkewButton control's skew angle to the slider.
                    skewSlider.Value = GetCurrentSkewAngle();
                }
            }
    
            // The following method handles the MouseLeftButtonDown event. 
            // It calls the BeginEdit method on the ModelItem which represents the 
            // Skewcontrol. 
            void slider_MouseLeftButtonDown(
                object sender, 
                System.Windows.Input.MouseButtonEventArgs e)
            {
                batchedChange = adornedControlModel.BeginEdit();
            }
    
            // The following method handles the MouseLeftButtonUp event. 
            // It commits any changes made to the ModelItem which represents the 
            // Skewcontrol. 
            void slider_MouseLeftButtonUp(
                object sender, 
                System.Windows.Input.MouseButtonEventArgs e)
            {
                if (batchedChange != null)
                {
                    batchedChange.Complete();
                    batchedChange.Dispose();
                    batchedChange = null;
                }
            }
    
            // The following method handles the slider control's  
            // ValueChanged event. It sets the value of the  
            // RenderTransform property by using the ModelProperty 
            // type. 
            void slider_ValueChanged(
                object sender, 
                RoutedPropertyChangedEventArgs<double> e)
            {
                double newSkewValue = e.NewValue;
    
                // During setup, don't make a value local and set the skew angle. 
                if (newSkewValue == GetCurrentSkewAngle())
                {
                    return;
                }
    
                // Access the SkewButton control's RenderTransform property 
                // by using the ModelProperty type.
                ModelProperty skewProperty = 
                    adornedControlModel.Properties[Control.RenderTransformProperty];
    
                if (!skewProperty.IsSet)
                {
                    // If the value isn't local, make it local  
                    // before setting a sub-property value.
                    skewProperty.SetValue(skewProperty.ComputedValue);
                }
    
                // Set the RenderTransform property on the SkewButton.
                skewProperty.SetValue(new SkewTransform(newSkewValue, 0));
            }
    
            // This utility method gets the SkewControl control's 
            // skew angle by using the ModelItem. 
            private double GetCurrentSkewAngle()
            {   
                SkewTransform skewXform = adornedControlModel.Properties[ 
                    Control.RenderTransformProperty].ComputedValue as SkewTransform;
    
                return skewXform.AngleX;
            }
        }
    }
    
  3. Build the solution.

Testing the Design-time Implementation

You can use the SkewButton control as you would use any other WPF control. The WPF Designer handles the creation of all design-time objects.

To test the design-time implementation

  1. Add a new WPF Application project named DemoApplication to the solution.

    Window1.xaml opens in the WPF Designer.

  2. Add a reference to the SkewButtonLibrary project.

  3. In XAML view, replace the automatically generated XAML with the following XAML. This XAML adds a reference to the SkewButtonLibrary namespace and adds the SkewButton custom control. The button appears in Design view with the text "Design mode active", indicating that it is in design mode. If the button does not appear, you might have to click the Information bar at the top of the designer to reload the view.

    <Window x:Class="DemoApplication.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cc="clr-namespace:SkewButtonLibrary;assembly=SkewButtonLibrary"
        Title="Window1" Height="300" Width="300">
        <Grid>
            <cc:SkewButton Margin="30,30,30,30" Background="#FFD4D0C8">
                <cc:SkewButton.RenderTransform>
                    <SkewTransform AngleX="0" AngleY="0" />
                </cc:SkewButton.RenderTransform>
            </cc:SkewButton>
        </Grid>
    </Window>
    
    <Window x:Class="DemoApplication.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cc="clr-namespace:SkewButtonLibrary;assembly=SkewButtonLibrary"
        Title="Window1" Height="300" Width="300">
        <Grid>
            <cc:SkewButton Margin="30,30,30,30" Background="#FFD4D0C8">
                <cc:SkewButton.RenderTransform>
                    <SkewTransform AngleX="0" AngleY="0" />
                </cc:SkewButton.RenderTransform>
            </cc:SkewButton>
        </Grid>
    </Window>
    
  4. In Design view, click the SkewButton control to select it.

    A Slider control appears inside the SkewButton control. If the slider control does not appear, try rebuilding the solution.

  5. Change the value of the slider control.

    The control skews as you drag the slider. In XAML view, the RenderTransform property is set to the value specified by the adorner.

  6. Run the DemoApplication project.

    At run time, the button is skewed at the angle you set with the adorner.

Next Steps

You can add more custom design-time features to your custom controls.

See Also

Tasks

How to: Implement a Rail Inside a Control

Reference

PrimarySelectionAdornerProvider

SkewTransform

Other Resources

Advanced Extensibility Concepts

WPF Designer Extensibility