Chapter 4. Creating User and Custom Controls

WPF provides developers with unparalleled options in customizing and modifying the visual appearance of controls.

In the first instance, you can simply assign custom values to the appearance properties of the built-in WPF controls. For example, you could set the Background property of a System.Windows.Controls.Button control to silver and the FontWeight property to bold.

If you wanted to reuse this Button control in different places within your application, you could define an application-wide System.Windows.Style to set these property values and then apply this Style to all Button objects automatically (see Chapter 6).

Alternatively, suppose you wanted every Button to display an image surrounded by a border. The content model in WPF makes this easy. Simply declare a System.Windows.Controls.Border and a System.Windows.Controls.Image in the inline XAML for your button. If you wanted to reuse this type of button across your application, you could define a System.Windows.Controls.ControlTemplate with an application-wide Style (see Chapter 6).

These mechanisms for changing the appearance offer a great deal of power and flexibility to change individual controls and elements. However, when you want to create reusable groups of controls and functionality, you need to create a user or custom control. User controls are ideal for situations where you need to encapsulate a group of visual elements and behaviors into one component that can be reused in different parts of your application.

However, because user controls encapsulate much of their visual appearance, you cannot change their style and control template in different contexts. This is where custom controls come in. They separate their interaction logic from their visual implementation, allowing other developers to reuse them within different applications and to customize their appearance themselves.

Finally, you can also create custom-drawn controls and render them to the screen using custom drawing logic.

This chapter focuses on how to create user and custom controls and custom-drawn elements, and it demonstrates some examples of all these types of controls. The recipes in this chapter describe how to:

Create a User Control

Problem

You need to create a user control to reuse part of the UI in different contexts within your application, without duplicating appearance or behavior logic.

Solution

Create a class that derives from System.Windows.Controls.UserControl or System.Windows.Controls.ContentControl, and place the visual elements you need in your reusable component in the XAML for the user control. Put custom logic in the code-behind for the UserControl to control custom behavior and functionality.

Tip

A control that derives from UserControl is useful for creating a reusable component within an application but is less useful if it can be shared by other applications, software teams, or even companies. This is because a control that derives from UserControl cannot have its appearance customized by applying custom styles and templates in the consumer. If this is needed, then you need to use a custom control, which is a control that derives from System.Windows.UIElement.FrameworkElement or System.Windows.Controls.Control.

How It Works

User controls provide a simple development model that is similar to creating WPF elements in standard windows. They are ideal for composing reusable UI controls out of existing components or elements, provided you do not need to allow them to be extensively customized by consumers of your control. If you do want to provide full control over the visual appearance of your control, or allow it to be a container for other controls, then a custom control is more suitable. Custom controls are covered later in this chapter.

To create a user control, right-click your project in Visual Studio, click Add, and then click the User Control option in the submenu. This creates a new XAML file and a corresponding code-behind file. The root element of the new XAML file is a System.Windows.Controls.UserControl class. Inside this XAML file, you can create the UI elements that compose your control.

The Code

The following example demonstrates how to create a FileInputControl, a custom reusable user control to encapsulate the functionality of browsing to a file and displaying the file name. This user control is then used in a window, as shown in Figure 4-1.

The XAML for the FileInputControl is as follows:

<UserControl x:Class="Recipe_04_01.FileInputControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <DockPanel>

        <Button
                DockPanel.Dock="Right"
                Margin="2,0,0,0"
                Click="BrowseButton_Click">
            Browse...
        </Button>

        <TextBox x:Name="txtBox"
                 IsReadOnly="True" />
    </DockPanel>
</UserControl>

The code-behind for the control is as follows:

using System.Windows.Controls;
using Microsoft.Win32;

namespace Recipe_04_01
{
     public partial class FileInputControl : UserControl
     {
         public FileInputControl()
         {
             InitializeComponent();
         }

         private void BrowseButton_Click(
             object sender,
             System.Windows.RoutedEventArgs e)
         {
             OpenFileDialog dlg = new OpenFileDialog();
             if(dlg.ShowDialog() == true)
             {
                 this.FileName = dlg.FileName;
             }
         }
public string FileName
         {
             get
             {
                return txtBox.Text;
             }
             set
             {
                txtBox.Text = value;
             }
         }
     }
}

The XAML for the window that consumes this user control is as follows:

<Window x:Class="Recipe_04_01.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_01="clr-namespace:Recipe_04_01;assembly="
     Title="WPF Recipes 4_01" Height="72" Width="300">
    <Grid>
        <Recipe_04_01:FileInputControl Margin="8" />
    </Grid>
</Window>
Creating and using a FileInput user control

Figure 4-1. Creating and using a FileInput user control

Set the Content Property of a User Control

Problem

You need to specify the Content property of your System.Windows.Controls.UserControl so that when the consumer defines an instance of your UserControl, the consumer can set the value of this property as the inline content.

Solution

Use the System.Windows.Markup.ContentPropertyAttribute attribute to decorate your user control's class declaration, and specify the name of the property you want to designate as the Content property.

How It Works

Because UserControl ultimately inherits from System.Windows.Controls.ContentControl, the Content property is the default property to receive the value of any inline XAML declarations. For example, a consumer of a FileInputControl (see the following code) might declare the instance of the control with the following XAML:

<local:FileInputControl>c:
eadme.txt</local:FileInputControl>

Without the ContentProperty attribute on the user control, this XAML declaration would replace the control elements inside the FileInputControl and simply display a string. The ContentProperty attribute tells the user control to instead use another property to set whenever a value is passed as inline content.

Warning

An explicit setting of the Content property would still replace the visual elements inside the control, for example, <local:FileInputControl Content="c: eadme.txt" />. If this is a real possibility and you need to prevent this case as well, then you should create a custom control rather than a user control and specify the visual elements of the control in a control template. In this case, you could use a template binding to bind TextBox.Text to the Content property.

The Code

The following example demonstrates how to set the Content property of a UserControl. It defines a UserControl called FileInputControl that can be used to browse to a file using the Microsoft.Win32.OpenFileDialog and to display the file name in a System.Windows.Controls.TextBox. In the code-behind, the FileInputControl class is decorated with the ContentProperty attribute and passed the name of the FileName property in the parameter of its constructor. The user control is then used in a window called Window1. In the XAML for this window, an initial file name is set by specifying the text as the inline content.

The XAML for the FileInputControl is as follows:

<UserControl x:Class="Recipe_04_02.FileInputControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <DockPanel>

        <Button
                DockPanel.Dock="Right"
                Margin="2,0,0,0"
                Click="BrowseButton_Click">
            Browse...
        </Button>
<TextBox x:Name="txtBox"
             IsReadOnly="True" />

    </DockPanel>

</UserControl>

The code-behind for the FileInputControl is as follows:

using System.Windows.Controls;
using System.Windows.Markup;
using Microsoft.Win32;

namespace Recipe_04_02
{
   /// <summary>
   /// ContentProperty attribute
   /// </summary>
   [ContentProperty("FileName")]
   public partial class FileInputControl : UserControl
   {
       public FileInputControl()
       {
           InitializeComponent();
       }

       private void BrowseButton_Click(
           object sender,
           System.Windows.RoutedEventArgs e)
       {
           OpenFileDialog dlg = new OpenFileDialog();
           if(dlg.ShowDialog() == true)
           {
               this.FileName = dlg.FileName;
           }
       }

       public string FileName
       {
           get
           {
               return txtBox.Text;
           }
           set
           {
                txtBox.Text = value;
           }
        }
    }
}

The following XAML shows how to use the FileInputControl in a window and declares a file name in the inline content of the declaration, which then automatically sets the value of the FileName property:

<Window x:Class="Recipe_04_02.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_02="clr-namespace:Recipe_04_02;assembly="
    Title="WPF Recipes 4_02" Height="72" Width="300">
    <Grid>
        <Recipe_04_02:FileInputControl
            Margin="8">
            c:
eadme.txt
        </Recipe_04_02:FileInputControl>
    </Grid>
</Window>

Add Properties to a User Control

Problem

You need to allow internal aspects of the behavior and appearance of your System.Windows.Controls.UserControl to be changed by the control consumer and to be accessible to WPF features such as data binding, styles, and animations.

Solution

Create a standard .NET property in the code-behind of your user control, and use it in the internal configuration of the control to determine aspects of behavior or appearance. Create a static System.Windows.DependencyProperty field, with the word Property added to the end of your property name, and use it to back the standard .NET property. Register the dependency property in the static constructor of the user control.

How It Works

By using a DependencyProperty to hold the value of behavioral or appearance properties of your user control, you can use the full range of WPF features such as data binding, styling, and animations to interact with these values.

The Code

The following example demonstrates how to use DependencyProperties to interact with a custom PageNumberControl that displays a descriptive page number string, for example, "Page 2 of 8."

The user control exposes Count and Total dependency properties in the code-behind, which are then used in the control's XAML to construct the display string.

using System.Windows;
using System.Windows.Controls;

namespace Recipe_04_03
{
    /// <summary>
    /// Show the page number text in the format:
    /// <!-- Page <Current> of <Total>
    /// </summary>
    public partial class PageNumberControl : UserControl
    {
        public PageNumberControl()
        {
            InitializeComponent();
        }

        public int Current
        {
            get
            {
                return (int) GetValue(CurrentProperty);
            }
            set
            {
                if(value <= Total
                  && value >= 0)
                {
                   SetValue(CurrentProperty, value);
                }
             }
         }

         public static readonly DependencyProperty CurrentProperty =
             DependencyProperty.Register("Current",
                                         typeof(int),
                                         typeof(PageNumberControl),
                                         new PropertyMetadata(0));

         public int Total
         {
             get
             {
                 return (int) GetValue(TotalProperty);
             }
set
     {
         if(value >= Current
            && value >= 0)
         {
            SetValue(TotalProperty, value);
         }
      }
   }

   public static readonly DependencyProperty TotalProperty =
       DependencyProperty.Register("Total",
                                   typeof(int),
                                   typeof(PageNumberControl),
                                   new PropertyMetadata(0));
    }
}

The XAML for the PageNumberControl is as follows:

<UserControl
    x:Class="Recipe_04_03.PageNumberControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="rootControl"
    Height="100" Width="200">
    <StackPanel
        Orientation="Horizontal"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Margin="10">

        <!-- Show the page number text in the format: -->
        <!-- Page <Current> of <Total> -->
        <TextBlock Text="Page "/>
        <TextBlock
            Text="{Binding
            ElementName=rootControl,
            Path=Current}"
            />
        <TextBlock Text=" of "/>
        <TextBlock
            Text="{Binding
            ElementName=rootControl,
            Path=Total}"
            />
    </StackPanel>
</UserControl>

The following XAML shows how to use the PageNumberControl in a window and contains buttons that, when clicked, change the Current and Total properties and automatically update the display. Figure 4-2 shows the resulting window.

<Window x:Class="Recipe_04_03.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_03="clr-namespace:Recipe_04_03;assembly="
    Title="WPF Recipes 4_03" Height="120" Width="260">
    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition Height="0.25*" />
            <RowDefinition Height="0.75*" />
        </Grid.RowDefinitions>

        <Recipe_04_03:PageNumberControl
            x:Name="pageNumberControl"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Margin="4"
            Current="2"
            Total="5"
            />

        <GroupBox Header="Test"
                  Margin="4"
                  Grid.Row="1"
                  HorizontalAlignment="Stretch"
                  VerticalAlignment="Stretch">
            <StackPanel
                Orientation="Horizontal">
                <Button Click="DecreaseCurrent_Click"
                    Margin="4">
                    Current--
                </Button>
                <Button Click="IncreaseCurrent_Click"
                    Margin="4">
                    Current++
                </Button>
                <Button Click="DecreaseTotal_Click"
                    Margin="4">
                    Total--
                </Button>
                <Button Click="IncreaseTotal_Click"
                    Margin="4">
                    Total++
                </Button>
</StackPanel>
        </GroupBox>

    </Grid>
</Window>

The code in the window's code-behind handles the click events of the buttons and simply increments or decrements the PageNumberControl's dependency properties:

using System.Windows;

namespace Recipe_04_03
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private void DecreaseCurrent_Click(object sender,
                                           RoutedEventArgs e)
        {
            pageNumberControl.Current--;
        }

        private void IncreaseCurrent_Click(object sender,
                                           RoutedEventArgs e)
        {
            pageNumberControl.Current++;
        }

        private void DecreaseTotal_Click(object sender,
                                         RoutedEventArgs e)
        {
            pageNumberControl.Total--;
        }

        private void IncreaseTotal_Click(object sender,
                                         RoutedEventArgs e)
        {
            pageNumberControl.Total++;
        }
    }
}
Using DependencyProperties in a PageNumberControl to allow the current and total page numbers to be manipulated

Figure 4-2. Using DependencyProperties in a PageNumberControl to allow the current and total page numbers to be manipulated

Add Events to a User Control

Problem

You need to notify the control consumer when something happens in your System.Windows.Controls.UserControl and allow it to use this event with WPF features such as triggers, animations, and event bubbling and tunneling.

Solution

Create a static property of type System.Windows.RoutedEvent in the code-behind of your user control, with the word Event added to the end of the name of the event you want to raise, and register it with the EventManager:

public static RoutedEvent SearchChangedEvent =
            EventManager.RegisterRoutedEvent(
                                             "SearchChanged",
                                             RoutingStrategy.Bubble,
                                             typeof(SearchChangedEventHandler),
                                             typeof(SearchControl));

Then use the RaiseEvent method of the base System.Windows.UIElement class to notify the consumer of the user control:

SearchChangedEventArgs args = new SearchChangedEventArgs(txtSearch.Text);
args.RoutedEvent = SearchChangedEvent;
RaiseEvent(args);

How It Works

By using a RoutedEvent to wrap an ordinary .NET event, you can expose this event to the consumer of your user control and allow it to use the full range of WPF features such as triggers, animations, and event bubbling and tunneling.

The Code

The following example demonstrates how to use a RoutedEvent to notify the control consumer when the search text is changed within a custom search user control. The SearchControl defined next contains a System.Windows.Controls.TextBox for entering a new search string, as well as a System.Windows.Controls.Button to raise a SearchChanged event. The SearchChanged event is also raised when the Enter key is pressed within the search TextBox. An instance of this SearchControl is defined in a window, and an event handler is added to the SearchChanged events, which displays the new search text in a System.Windows.MessageBox.

The XAML for the SearchControl user control is as follows:

<UserControl x:Class="Recipe_04_04.SearchControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="Auto" Width="Auto">

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="SearchImage.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>

    <Grid>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="48"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>

        <TextBlock>
            Enter your search text:
        </TextBlock>

        <TextBox
            x:Name="txtSearch"
            KeyDown="txtSearch_KeyDown"
            Grid.Row="1"/>

        <Button Grid.Column="1"
                Grid.RowSpan="2"
                Margin="4,0,0,0"
                Click="SearchButton_Click">
            <Image Source="{StaticResource SearchImage}"/>
        </Button>

    </Grid>
</UserControl>

The code-behind declares the SearchChanged RoutedEvent:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Recipe_04_04
{
    /// <summary>
    /// A reusable Search UserControl that raises a
    /// RoutedEvent when a new search is requested.
    /// </summary>
    public partial class SearchControl : UserControl
    {
        public SearchControl()
        {
            InitializeComponent();
        }

        public static RoutedEvent SearchChangedEvent =
            EventManager.RegisterRoutedEvent(
                "SearchChanged",
                RoutingStrategy.Bubble,
                typeof(SearchChangedEventHandler),
                typeof(SearchControl));

       /// <summary>
       /// The SearchChanged event that can be handled
       /// by the consuming control.
       /// </summary>
       public event SearchChangedEventHandler SearchChanged
       {
           add
           {
               AddHandler(SearchChangedEvent, value);
           }
           remove
           {
               RemoveHandler(SearchChangedEvent, value);
           }
        }

        private void SearchButton_Click(
            object sender,
            RoutedEventArgs e)
        {
            // Raise the SearchChanged RoutedEvent when
            // the Search button is clicked
OnSearchChanged();
       }

       private void txtSearch_KeyDown(
           object sender,
           KeyEventArgs e)
       {
           if(e.Key == Key.Enter)
           {
               // Raise the SearchChanged RoutedEvent when
               // the Enter key is pressed in the Search TextBox
               OnSearchChanged();
           }
        }

        private void OnSearchChanged()
        {
            SearchChangedEventArgs args =
                new SearchChangedEventArgs(txtSearch.Text);
            args.RoutedEvent = SearchChangedEvent;
            RaiseEvent(args);
        }
     }

     public delegate void SearchChangedEventHandler(
         object sender,
         SearchChangedEventArgs e);

     public class SearchChangedEventArgs
         : RoutedEventArgs
     {
         private readonly string searchText;

         public SearchChangedEventArgs(
             string searchText)
         {
              this.searchText = searchText;
         }

         public string SearchText
         {
             get
             {
                 return searchText;
             }
         }
     }
}

The following XAML shows how to use the SearchControl in a window and adds an event handler to the SearchChanged event. Figure 4-3 shows the resulting window.

<Window x:Class="Recipe_04_04.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_04="clr-namespace:Recipe_04_04;assembly="
    Title="WPF Recipes 4_04" Height="86" Width="240">

    <Grid>
        <Recipe_04_04:SearchControl
            Margin="8"
            SearchChanged="SearchControl_SearchChanged"/>
    </Grid>
</Window>

The code in the code-behind for the window handles the SearchControl's SearchChanged RoutedEvent and shows the new search text in a message box:

using System.Windows;
using Recipe_04_04;

namespace Recipe_04_04
{
    /// <summary>
    /// This window creates an instance of SearchControl
    /// and handles the SearchChanged event, showing the
    /// new search text in a message box
    /// </summary>
    public partial class Window1 : Window
    {

        public Window1()
        {
            InitializeComponent();
        }

        private void SearchControl_SearchChanged(
            object sender,
            SearchChangedEventArgs e)
        {
            MessageBox.Show("New Search: " + e.SearchText);
        }
    }
}
Using a RoutedEvent in a reusable search user control

Figure 4-3. Using a RoutedEvent in a reusable search user control

Support Application Commands in a User Control

Problem

You need to support common application commands in your System.Windows.Controls.UserControl, such as Undo, Redo, Open, Copy, Paste, and so on, so that your control can respond to a command without needing any external code.

Solution

Use the System.Windows.Input.CommandManager to register an instance of the System.Windows.Input.CommandBinding class for each member of System.Windows.Input.ApplicationCommands you need to support in your user control. The CommandBinding specifies the type of command you want to receive notification of, specifies an event handler to determine when the command can be executed, and specifies another event handler to be called when the command is executed. These event handlers are called the CanExecute and Executed event handlers.

How It Works

There are many predefined commands in WPF to support common scenarios, grouped as static properties on five different classes, mostly in the System.Windows.Input namespace, as shown in Table 4-1.

Table 4-1. Predefined Common Commands

Value

Description

ApplicationCommands

Common commands for an application, for example, Copy, Paste, Undo, Redo, Find, Open, SaveAs, Print, and so on

ComponentCommands

Common commands for user interface components, for example, MoveLeft, MoveToEnd, ScrollPageDown, and so on

MediaCommands

Common commands used for multimedia, for example, Play, Pause, NextTrack, IncreaseVolume, ToggleMicrophoneOnOff, and so on

NavigationCommands

A set of commands used for page navigation, for example, BrowseBack, GoToPage, NextPage, Refresh, Zoom, and so on

EditingCommands

A set of commands for editing documents, for example, AlignCenter, IncreaseFontSize, EnterParagraphBreak, ToggleBold, and so on

Each command has a System.Windows.Input.InputGestureCollection that specifies the possible mouse or keyboard combinations that trigger the command. These are defined by the command itself, which is why you are able to register to receive these automatically by registering a CommandBinding for a particular command.

A CommandBinding for a particular command registers the CanExecute and Executed handlers so that the execution and the validation of the execution of the command are routed to these event handlers.

The Code

The following example creates a UserControl called FileInputControl that can be used to browse to a file using Microsoft.Win32.OpenFileDialog and display the file name in a System.Windows.Controls.TextBox.

It registers a CommandBinding for two application commands, Open and Find. When the user control has focus and the keyboard shortcuts for the Open and Find command (Ctrl+O and Ctrl+F, respectively) are used, the Executed event handler for the respective command is invoked.

The Executed event handler for the Find command launches the OpenFileDialog, as if the user has clicked the Browse button. This command can always be executed, so the CanExecute event handler simply sets the CanExecute property of System.Windows.Input.CanExecuteRoutedEventArgs to True.

The Executed event handler for the Open command launches the file that is currently displayed in the TextBox. Therefore, the CanExecute event handler for this command sets the CanExecuteRoutedEventArgs to True only if there is a valid FileName.

The XAML for the FileInputControl is as follows:

<UserControl x:Class="Recipe_04_05.FileInputControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <DockPanel>

        <Button
                DockPanel.Dock="Right"
                Margin="2,0,0,0"
                Click="BrowseButton_Click">
            Browse...
        </Button>

        <TextBox x:Name="txtBox" />

    </DockPanel>
</UserControl>

The code-behind for the FileInputControl is as follows:

using System.Diagnostics;
using System.IO;
using System.Windows.Controls;
using System.Windows.Input;
using Microsoft.Win32;

namespace Recipe_04_05
{
    public partial class FileInputControl : UserControl
    {
        public FileInputControl()
        {
            InitializeComponent();

            // Register command bindings

            // ApplicationCommands.Find
            CommandManager.RegisterClassCommandBinding(
                typeof(FileInputControl),
                new CommandBinding(
                    ApplicationCommands.Find,
                    FindCommand_Executed,
                    FindCommand_CanExecute));

            // ApplicationCommands.Open
            CommandManager.RegisterClassCommandBinding(
                typeof(FileInputControl),
                new CommandBinding(
                    ApplicationCommands.Open,
                    OpenCommand_Executed,
                    OpenCommand_CanExecute));
    }

    #region Find Command

    private void FindCommand_CanExecute(
        object sender,
        CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = true;
    }

    private void FindCommand_Executed(
        object sender,
        ExecutedRoutedEventArgs e)
    {
        DoFindFile();
    }

    #endregion
#region Open Command

       private void OpenCommand_CanExecute(
           object sender,
           CanExecuteRoutedEventArgs e)
       {
           e.CanExecute =
               !string.IsNullOrEmpty(this.FileName)
           && File.Exists(this.FileName);
       }

       private void OpenCommand_Executed(
           object sender,
           ExecutedRoutedEventArgs e)
       {
           Process.Start(this.FileName);
       }

       #endregion

       private void BrowseButton_Click(
           object sender,
           System.Windows.RoutedEventArgs e)
       {
           DoFindFile();
       }

       private void DoFindFile()
       {
           OpenFileDialog dlg = new OpenFileDialog();
           if(dlg.ShowDialog() == true)
           {
               this.FileName = dlg.FileName;
           }
       }

       public string FileName
       {
           get
           {
               return txtBox.Text;
           }
set
         {
           txtBox.Text = value;
         }
      }
   }
}

The following XAML shows how to use the FileInputControl in a window.

If the TextBox has the focus, then pressing the keyboard shortcut Ctrl+F will automatically open the OpenFileDialog. If a file is selected and a valid file name appears in the TextBox, then the shortcut Ctrl+O will launch it.

<Window x:Class="Recipe_04_05.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_05="clr-namespace:Recipe_04_05;assembly="
    Title="WPF Recipes 4_05" Height="72" Width="300">
    <Grid>
        <Recipe_04_05:FileInputControl
            Margin="8"/>
    </Grid>
</Window>

Add Custom Commands to a User Control

Problem

You need to add custom commands to your System.Windows.Controls.UserControl to enable consumers of your control to bind to and execute units of functionality and custom behavior.

Solution

Create a static System.Windows.Input.RoutedCommand property in the code-behind of your user control. In the static constructor, initialize a class-level instance of this RoutedCommand and give it a name, the type of your user control, and any input gestures you want to associate with it. A System.Windows.Input.InputGesture associates keyboard and mouse inputs with your commands so that when a certain key combination is pressed, for example, Ctrl+W, the System.Windows.Input.CommandManager will execute your command.

Create an instance of the System.Windows.Input.CommandBinding class for your RoutedCommand, and specify an event handler to determine when the command can be executed and another event handler to be called when the command is executed.

Consumers of your control can now define visual elements that data bind directly to your static command property.

How It Works

Three types of command classes in WPF support data binding, and they can all be found in the System.Windows.Input namespace (see Table 4-2).

Table 4-2. Three Types of WPF Commands

Value

Description

ICommand

The basic command interface in WPF. This exposes two methods, Execute and CanExecute, and a CanExecuteChanged event.

RoutedCommand

Implements ICommand and adds support for event tunneling and bubbling and input gestures.

RoutedUICommand

Derives from RoutedCommand and adds a localizable Text property.

Creating a RoutedCommand or RoutedUICommand allows you to expose custom command functionality and automatically plug in to the event tunneling and bubbling mechanisms in WPF that route a consumer of your command to your custom event handlers.

The Code

The following example creates a user control called PageNumberControl that displays a descriptive page number string, for example, "Page 2 of 8."

The code-behind for the user control exposes a public RoutedCommand property called IncreaseTotal, which increases the total number of pages when executed. The static constructor initializes a CommandBinding that binds this command to the CanExecute and Executed event handlers.

The control is then consumed in a window, which demonstrates how to bind a System.Windows.Controls.Button to the custom command in XAML.

The XAML for the PageNumberControl is as follows:

<UserControl
    x:Class="Recipe_04_06.PageNumberControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="rootControl"
    Height="100" Width="200">
    <StackPanel
        Orientation="Horizontal"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Margin="10">

        <!-- Show the page number text in the format: -->
        <!-- Page <Current> of <Total> -->
        <TextBlock Text="Page "/>
        <TextBlock
Text="{Binding
        ElementName=rootControl,
        Path=Current}"
        />
    <TextBlock Text=" of "/>
    <TextBlock
        Text="{Binding
        ElementName=rootControl,
        Path=Total}"
        />
    </StackPanel>
</UserControl>

The code-behind for the PageNumberControl is as follows:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Recipe_04_06
{
    /// <summary>
    /// Show the page number text in the format:
    /// <!-- Page <Current> of <Total>
    /// </summary>
    public partial class PageNumberControl : UserControl
    {
        private static RoutedCommand increaseTotalCommand;

        public static RoutedCommand IncreaseTotal
        {
            get
            {
                return increaseTotalCommand;
            }
        }

        static PageNumberControl()
        {
        // Create an input gesture so that the command
        // is executed when the Add (+) key is pressed
        InputGestureCollection myInputs =
            new InputGestureCollection();
        myInputs.Add(
            new KeyGesture(
                Key.Add,
                ModifierKeys.Control));
// Create a RoutedCommand
       increaseTotalCommand =
           new RoutedCommand(
               "IncreaseTotal",
               typeof(PageNumberControl),
               myInputs);

       // Create a CommandBinding, specifying the
       // Execute and CanExecute handlers
       CommandBinding binding =
           new CommandBinding();

       binding.Command = increaseTotalCommand;
       binding.Executed +=
           new ExecutedRoutedEventHandler(binding_Executed);
       binding.CanExecute +=
           new CanExecuteRoutedEventHandler(binding_CanExecute);

       // Register the CommandBinding
       CommandManager.RegisterClassCommandBinding(
           typeof(PageNumberControl), binding);
   }

   public PageNumberControl()
   {
       InitializeComponent();
   }

   static void binding_CanExecute(
       object sender,
       CanExecuteRoutedEventArgs e)
   {
       // The command can execute as long as the
       // Total is less than the maximum integer value
       PageNumberControl control = (PageNumberControl) sender;
       e.CanExecute = control.Total < int.MaxValue;
   }

   private static void binding_Executed(
       object sender,
       ExecutedRoutedEventArgs e)
   {
       // Increment the value of Total when
       // the command is executed
       PageNumberControl control = (PageNumberControl) sender;
       control.Total++;
   }
public int Current
       {
         get
         {
             return (int) GetValue(CurrentProperty);
         }
         set
         {
             if(value <= Total
                && value >= 0)
             {
                SetValue(CurrentProperty, value);
             }
         }
      }

      public static readonly DependencyProperty CurrentProperty =
          DependencyProperty.Register("Current",
                                      typeof(int),
                                      typeof(PageNumberControl));

      public int Total
      {
          get
          {
              return (int) GetValue(TotalProperty);
          }
          set
          {
              if(value >= Current
                 && value >= 0)
              {
                 SetValue(TotalProperty, value);
              }
          }
       }

       public static readonly DependencyProperty TotalProperty =
           DependencyProperty.Register("Total",
                                       typeof(int),
                                       typeof(PageNumberControl));
    }
}

The following XAML shows how to use the PageNumberControl in a window, with a Button control that data binds to the IncreaseTotal command:

<Window x:Class="Recipe_04_06.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_06="clr-namespace:Recipe_04_06;assembly="
    Title="WPF Recipes 4_06" Height="120" Width="260">
    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition Height="0.25*" />
            <RowDefinition Height="0.75*" />
        </Grid.RowDefinitions>

        <Recipe_04_06:PageNumberControl
            x:Name="pageNumberControl"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Margin="4"
            Current="2"
            Total="5"
            />

        <GroupBox Header="Test"
                  Margin="4"
                  Grid.Row="1"
                  HorizontalAlignment="Stretch"
                  VerticalAlignment="Stretch">
            <StackPanel
                Orientation="Horizontal">

                <Button
                    Command="Recipe_04_06:PageNumberControl.IncreaseTotal"
                    CommandTarget=
                        "{Binding ElementName=pageNumberControl}"
                    Margin="4">
                    Total++
                </Button>
            </StackPanel>
        </GroupBox>

    </Grid>
</Window>

Figure 4-4 shows the resulting window.

Data binding to a custom RoutedCommand in a user control

Figure 4-4. Data binding to a custom RoutedCommand in a user control

Set Design Mode Behavior in a User Control

Problem

You need to determine whether your System.Windows.Controls.UserControl is running in design mode (for example, being displayed in the Visual Studio or Expression Blend designer) and set specific behavior.

Solution

Use the System.ComponentModel.DesignerProperties.GetIsInDesignMode method in the constructor for your user control.

How It Works

The static System.ComponentModel.DesignerProperties exposes an IsInDesignMode attached property that returns true if the control is currently running in design mode.

Tip

Setting specific behavior for your user control when it is in design mode can be useful for priming your user control with the kind of data or property values that would normally be set only at runtime. This enables your control to display itself realistically for designers, even when there is no actual data or property values available for it during design.

The Code

The following example demonstrates a simple user control called MyUserControl that contains a button with some text as Content. The constructor for the control calls the GetIsInDesignMode method and changes the button's Text property depending on whether it is currently being displayed in design mode.

The XAML for the control is as follows:

<UserControl x:Class="Recipe_04_07.MyUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Button x:Name="btnMode">
            Set Design Mode Behavior
        </Button>
    </Grid>
</UserControl>

The code-behind for the control calls the GetIsInDesignMode:

using System.Windows.Controls;

namespace Recipe_04_07
{
    public partial class MyUserControl : UserControl
    {
        public MyUserControl()
        {
            InitializeComponent();

            // Call the GetIsInDesignMode method
            if(System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
            {
                btnMode.Content = "In Design Mode";
            }
            else
            {
                btnMode.Content = "Runtime";
            }
        }
    }
}

Figure 4-5 shows the control when displayed by the Visual Studio designer.

Displaying the user control in design mode

Figure 4-5. Displaying the user control in design mode

Figure 4-6 shows the control when displayed at runtime.

Displaying the user control in a window at runtime

Figure 4-6. Displaying the user control in a window at runtime

Create a Lookless Custom Control

Problem

You need to create a custom control that encapsulates functionality and behavior logic but that can have its visual appearance changed by consumers. For example, you need consumers to be able to change the style, template, or visual theme of your control for a particular context, application, or operating system theme.

Solution

Create a lookless custom control class that contains interaction and behavior logic but little or no assumptions about its visual implementation. Then declare the default visual elements for it in a control template within a default style.

Tip

When creating the code for a custom control, you need to ensure it is lookless and assumes as little as possible about the actual implementation of the visual elements in the control template, because it could well be different across different consumers. This means ensuring that the UI is decoupled from the interaction logic by using commands and bindings, avoiding event handlers, and referencing elements in the ControlTemplate whenever possible.

How It Works

The first step in creating a lookless custom control is choosing which control to inherit from. You should derive from the most basic option available to you, because it provides the minimum required functionality and gives the control consumer the maximum freedom. On the other hand, it also makes sense to leverage as much built-in support as possible by deriving from an existing WPF control if it possesses similar behavior and functionality to your custom control. For example, if your control will be clickable, then it might make sense to inherit from the Button class. If your control is not only clickable but also has the notion of being in a selected or unselected state, then it might make sense to inherit from ToggleButton.

Some of the main base classes you can choose from are listed in Table 4-3.

Table 4-3. Main Base Classes for Creating a Custom Control

Name

Description

FrameworkElement

This is usually the most basic element from which you will derive. Use this when you need to draw your own element by overriding the OnRender method and explicitly defining the component visuals. FrameworkElement classes tend not to interact with the user; for example, the WPF Image and Border controls are FrameworkElement classes.

Control

Control is the base class used by most of the existing WPF controls. It allows you to define its appearance by using control templates, and it adds properties for setting the background and foreground, font, padding, tab index, and alignment of content. It also supports double-clicking through the MouseDoubleClick and PreviewMouseDoubleClick events.

ContentControl

This inherits from Control and adds a Content property that provides the ability to contain a single piece of content, which could be a string or another visual element. For example, a button ultimately derives from ContentControl, which is why it has the ability to contain any arbitrary visual element such as an image. Use this as your base class if you need your control to contain other objects defined by the control consumer.

Panel

This has a property called Children that contains a collection of System.Windows.UIElements, and it provides the layout logic for positioning these children within it.

Decorator

This wraps another control to decorate it with a particular visual effect or feature. For example, the Border is a Decorator control that draws a line around an element.

After choosing an appropriate base class for your custom control, you can create the class and put the logic for the interaction, functionality, and behavior of your control in the custom control class.

However, don't define your visual elements in a XAML file for the class, like you would with a user control. Instead, put the default definition of visual elements in a System.Windows.ControlTemplate, and declare this ControlTemplate in a default System.Windows.Style.

The next step is to specify that you will be providing this new style; otherwise, your control will continue to use the default template of its base class. You specify this by calling the OverrideMetadata method of DefaultStyleKeyProperty in the static constructor for your class.

Next, you need to place your style in the Generic.xaml resource dictionary in the Themes subfolder of your project. This ensures it is recognized as the default style for your control. You can also create other resource dictionaries in this subfolder, which enables you to target specific operating systems and give your custom controls a different visual appearance for each one.

Tip

When a custom control library contains several controls, it is often better the keep their styles separate instead of putting them all in the same Generic.xaml resource dictionary. You can use resource dictionary merging to keep each style in a separate resource dictionary file and then merge them into the main Generic.xaml one.

The custom style and template for your control must use the System.Type.TargetType attribute to attach it to the custom control automatically.

Tip

In Visual Studio, when you add a new WPF custom control to an existing project, it does a number of the previous steps for you. It automatically creates a code file with the correct call to DefaultStyleKeyproperty.OverrideMetadata. It creates the Themes subfolder and Generic.xaml resource dictionary if they don't already exist, and it defines a placeholder Style and ControlTemplate in there.

When creating your custom control class and default control template, you have to remember to make as few assumptions as possible about the actual implementation of the visual elements. This is in order to make the custom control as flexible as possible and to give control consumers as much freedom as possible when creating new styles and control templates.

You can enable this separation between the interaction logic and the visual implementation of your control in a number of ways.

First, when binding a property of a visual element in the default ControlTemplate to a dependency property of the control, use the System.Windows.Data.RelativeSource property instead of naming the element and referencing it via the ElementName property.

Second, instead of declaring event handlers in the XAML for the template, for example, for the Click event of a Button, either add the event handler programmatically in the control constructor or bind to commands. If you choose to use event handlers and bind them program-matically, override the OnApplyTemplate method and locate the controls dynamically.

Furthermore, give names only to those elements that without which the control would not be able to function as intended. By convention, give these intrinsic elements the name PART_ElementName so that they can be identified as part of the public interface for your control. For example, it is intrinsic to a ProgressBar that it has a visual element representing the total value at completion and a visual element indicating the relative value of the current progress. The default ControlTemplate for the System.Windows.Controls.ProgressBar therefore defines two named elements, PART_Track and PART_Indicator. These happen to be Border controls in the default template, but there is no reason why a control consumer could not provide a custom template that uses different controls to display these functional parts.

Tip

If your control requires named elements, as well as using the previously mentioned naming convention, apply the System.Windows.TemplatePart attribute to your control class, which documents and signals this requirement to users of your control and to design tools such as Expression Blend.

The following code example demonstrates how to separate the interaction logic and the visual implementation using these methods.

The Code

The following example demonstrates how to create a lookless custom control to encapsulate the functionality of browsing to a file and displaying the file name. Figure 4-7 shows the control in use.

The FileInputControl class derives from Control and uses the TemplatePart attribute to signal that it expects a Button control called PART_Browse. It overrides the OnApplyTemplate method and calls GetTemplateChild to find the button defined by its actual template. If this exists, it adds an event handler to the button's Click event.

The code for the control is as follows:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using Microsoft.Win32;

namespace Recipe_04_08
{
    [TemplatePart(Name = "PART_Browse", Type = typeof(Button))]
    [ContentProperty("FileName")]
    public class FileInputControl : Control
    {
        static FileInputControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(FileInputControl),
                new FrameworkPropertyMetadata(
                    typeof(FileInputControl)));
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            Button browseButton = base.GetTemplateChild("PART_Browse") as Button;

            if(browseButton != null)
                browseButton.Click += new RoutedEventHandler(browseButton_Click);
        }

        void browseButton_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog dlg = new OpenFileDialog();
            if(dlg.ShowDialog() == true)
            {
                this.FileName = dlg.FileName;
            }
        }
public string FileName
    {
       get
       {
           return (string) GetValue(FileNameProperty);
       }
       set
       {
           SetValue(FileNameProperty, value);
       }
    }

    public static readonly DependencyProperty FileNameProperty =
        DependencyProperty.Register (
                       "FileName",
                       typeof(string),
                       typeof(FileInputControl));
   }
}

The default style and control template for FileInputControl is in a ResourceDictionary in the Themes subfolder and is merged into the Generic ResourceDictionary. The XAML for this style is as follows:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_08="clr-namespace:Recipe_04_08;assembly=">

    <Style TargetType="{x:Type Recipe_04_08:FileInputControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate
                      TargetType="{x:Type Recipe_04_08:FileInputControl}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <DockPanel>

                            <Button
                                x:Name="PART_Browse"
                                DockPanel.Dock="Right"
                                Margin="2,0,0,0">
                                Browse...
                            </Button>
<TextBox
                               IsReadOnly="True"
                               Text="{Binding
                                   Path=FileName,
                                   RelativeSource=
                                       {RelativeSource TemplatedParent}}"
                               />

                        </DockPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

The XAML for the window that consumes this custom control is as follows:

<Window x:Class="Recipe_04_08.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_08="clr-namespace:Recipe_04_08;assembly="
   Title="WPF Recipes 4_08" Height="72" Width="300">
    <Grid>
        <Recipe_04_08:FileInputControl
            Margin="8"
            />
    </Grid>
</Window>
Creating and using a FileInput custom control

Figure 4-7. Creating and using a FileInput custom control

Specify the Parts Required by a Custom Control

Problem

You need to specify that consumers of your custom control should define certain elements within the control template in order for the control to function correctly.

Solution

In the default control template for your custom control, name any elements that are required by your control to function correctly, according to the naming convention PART_ElementName.

You should then document each part's existence by marking your class with System.Windows.TemplatePartAttribute, specifying the name and System.Type as parameters.

In the code for your custom control, add any event handlers to these elements dynamically by overriding the OnApplyTemplate method and locating the actual implementation of the element by calling GetTemplateChild.

How It Works

By documenting any parts required by your custom control using TemplatePartAttribute, you signal this requirement to consumers of your control and to design tools such as Expression Blend.

Furthermore, by attaching any necessary event handlers to the control parts programmatically, you ensure that they do not have to be specified in every template defined for your control by a consumer.

Tip

When locating these parts in the code for your custom control, it is recommended that you handle any omissions gracefully. If a template does not define a specific element, it should not cause an exception in your code. This not only allows consumers of your control to support just the functionality they require, but it also prevents issues when your control is used within design tools such as Expression Blend.

The Code

The following example demonstrates how to create a lookless custom control to encapsulate the functionality of browsing to a file and displaying the file name. Figure 4-8 shows the control in use.

The FileInputControl class derives from Control and uses the TemplatePart attribute to signal that it expects a Button control called PART_Browse. It overrides the OnApplyTemplate method and calls GetTemplateChild to find the button defined by its actual template. If this exists, it adds an event handler to the button's Click event.

The code for the control is as follows:

using System.Windows;
using System.Windows.Controls;
using Microsoft.Win32;

namespace Recipe_04_09
{
    /// <summary>
    /// The TemplatePart attribute specifies that the control
    /// expects the Control Template to contain a Button called
    /// PART_Browse
    /// </summary>
    [TemplatePart(Name = "PART_Browse", Type = typeof(Button))]
public class FileInputControl : Control
   {
      static FileInputControl()
      {
          DefaultStyleKeyProperty.OverrideMetadata(
              typeof(FileInputControl),
              new FrameworkPropertyMetadata(
                  typeof(FileInputControl)));
      }

      public override void OnApplyTemplate()
      {
          base.OnApplyTemplate();

          // Use the GetTemplateChild method to locate
          // the button called PART_Browse
          Button browseButton = base.GetTemplateChild("PART_Browse") as Button;

          // Do not cause or throw an exception
          // if it wasn't supplied by the Template
          if(browseButton != null)
             browseButton.Click += new RoutedEventHandler(browseButton_Click);
      }

      void browseButton_Click(object sender, RoutedEventArgs e)
      {
          OpenFileDialog dlg = new OpenFileDialog();
          if(dlg.ShowDialog() == true)
          {
              this.FileName = dlg.FileName;
          }
     }

     public string FileName
     {
         get
         {
             return (string) GetValue(FileNameProperty);
         }
         set
         {
             SetValue(FileNameProperty, value);
         }
     }

     public static readonly DependencyProperty FileNameProperty =
         DependencyProperty.Register(
"FileName",
            typeof(string),
            typeof(FileInputControl));
    }
}

The default style and control template for FileInputControl is in a ResourceDictionary in the Themes subfolder and is merged into the generic ResourceDictionary. The XAML for this style is as follows:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_09="clr-namespace:Recipe_04_09;assembly=">

    <Style TargetType="{x:Type Recipe_04_09:FileInputControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate
                      TargetType="{x:Type Recipe_04_09:FileInputControl}">
                    <Border Background="{TemplateBinding Background}"
                          BorderBrush="{TemplateBinding BorderBrush}"
                          BorderThickness="{TemplateBinding BorderThickness}">
                      <DockPanel>

                          <Button
                              x:Name="PART_Browse"
                              DockPanel.Dock="Right"
                              Margin="2,0,0,0">
                              Browse...
                          </Button>

                          <TextBox
                              IsReadOnly="True"
                              Text="{Binding
                                 Path=FileName,
                                 RelativeSource=
                                    {RelativeSource TemplatedParent}}"
                              />

                        </DockPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

The XAML for the window that consumes this custom control is as follows:

<Window x:Class="Recipe_04_09.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_09="clr-namespace:Recipe_04_09;assembly="
   Title="WPF Recipes 4_09" Height="72" Width="300">
    <Grid>
        <Recipe_04_09:FileInputControl
            Margin="8"
            />
    </Grid>
</Window>
Creating and using a FileInput custom control

Figure 4-8. Creating and using a FileInput custom control

Support UI Automation in a Custom Control

Problem

You need to support UI automation in your custom control to allow test scripts to interact with the UI.

Solution

Create a companion class called ControlNameAutomationPeer for your custom control that derives from System.Windows.Automation.Peers.FrameworkElementAutomationPeer.

Override the OnCreateAutomationPeer method in your custom control, and return an instance of your companion class.

How It Works

The FrameworkElementAutomationPeer companion class describes your control to the automation system. Whenever an event occurs that should be communicated to the automation system, you can retrieve the companion class and call the Invoke method of System.Windows.Automation.Provider.IInvokeProvider.

The Code

The following example demonstrates how to create a lookless custom control to encapsulate the functionality of browsing to a file and displaying the file name.

The code for the control also defines a class called FileInputControlAutomationPeer that provides UI automation support and returns an instance of this class in the OnCreateAutomationPeer method.

The code for the control is as follows:

using System;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;
using System.Windows.Markup;
using Microsoft.Win32;

namespace Recipe_04_10
{
    [TemplatePart(Name = "PART_Browse", Type = typeof(Button))]
    [ContentProperty("FileName")]
    public class FileInputControl : Control
    {
        static FileInputControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(FileInputControl),
                new FrameworkPropertyMetadata(
                    typeof(FileInputControl)));
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            Button browseButton = base.GetTemplateChild("PART_Browse") as Button;

            if(browseButton != null)
                browseButton.Click += new RoutedEventHandler(browseButton_Click);
        }

        void browseButton_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog dlg = new OpenFileDialog();
            if(dlg.ShowDialog() == true)
            {
                this.FileName = dlg.FileName;
            }
        }
public string FileName
     {
        get
        {
            return (string) GetValue(FileNameProperty);
        }
        set
        {
            SetValue(FileNameProperty, value);
        }
     }

     public static readonly DependencyProperty FileNameProperty =
         DependencyProperty.Register(
                   "FileName",
                   typeof(string),
                   typeof(FileInputControl));

     /// <summary>
     /// Identifies SimpleButton.Click routed event.
     /// </summary>
     public static readonly RoutedEvent ClickEvent
               = EventManager.RegisterRoutedEvent(
                         "Click",
                         RoutingStrategy.Bubble,
                         typeof(EventHandler),
                         typeof(FileInputControl));

     /// <summary>
     /// Occurs when a Simple button is clicked.
     /// </summary>
     public event RoutedEventHandler Click
     {
         add
         {
             AddHandler(ClickEvent, value);
         }
         remove
         {
             RemoveHandler(ClickEvent, value);
         }
     }

     /// <summary>
     /// Overriding of this method provides an UI Automation support
     /// </summary>
     /// <returns></returns>
protected override AutomationPeer OnCreateAutomationPeer()
     {
        return new FileInputControlAutomationPeer(this);
     }
  }

  /// <summary>
  /// Class that provides UI Automation support
  /// </summary>
  public class FileInputControlAutomationPeer
      : FrameworkElementAutomationPeer,
        IInvokeProvider
  {
      public FileInputControlAutomationPeer(FileInputControl control)
          : base(control)
      {
      }

      protected override string GetClassNameCore()
     {
          return "FileInputControl";
     }

     protected override string GetLocalizedControlTypeCore()
     {
         return "FileInputControl";
     }

     protected override AutomationControlType GetAutomationControlTypeCore()
     {
         return AutomationControlType.Button;
     }

     public override object GetPattern(PatternInterface patternInterface)
     {
         if(patternInterface == PatternInterface.Invoke)
         {
             return this;
         }

         return base.GetPattern(patternInterface);
    }

    private FileInputControl MyOwner
    {
get
         {
            return (FileInputControl) base.Owner;
         }
      }

      #region IInvokeProvider Members

      public void Invoke()
      {
          RoutedEventArgs newEventArgs
                       = new RoutedEventArgs(FileInputControl.ClickEvent);
          MyOwner.RaiseEvent(newEventArgs);
      }
      #endregion
   }
}

The control is then used in the following window. It contains a button that, when clicked, invokes the Click event of the FileInputControl via the automation peer.

The code-behind for this window is as follows:

using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using Recipe_04_10;

namespace Recipe_04_10
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            ctlFileInput.Click +=
                new RoutedEventHandler(ctlFileInput_Click);
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // Get the AutomationPeer for this control
            FileInputControlAutomationPeer peer =
                new FileInputControlAutomationPeer(ctlFileInput);

            IInvokeProvider invokeProvider =
                peer.GetPattern(PatternInterface.Invoke)
                as IInvokeProvider;
// Call the Invoke method
         invokeProvider.Invoke();
      }

      private void ctlFileInput_Click(
          object sender,
          RoutedEventArgs e)
      {
          MessageBox.Show("Invoked via the Automation Peer");
      }
   }
}

Create a Custom-Drawn Element

Problem

You need to be able to draw a custom element.

Solution

Create a class that derives from System.Windows.FrameworkElement, and override the OnRender method of the base class System.Windows.UIElement. Add code to render the custom element to the System.Windows.Media.DrawingContext.

How It Works

The OnRender method of UIElement is provided with a DrawingContext. This context provides methods for drawing text and geometries.

When the parent of the UIElement detects that the size has changed, OnRender is called automatically. It can also be invoked when any data or properties change by calling the InvalidateVisual method of UIElement.

The Code

The following example demonstrates how to render a custom pie chart control using the OnRender method.

The PieChartControl contains a Slices property that tells the control the slices it needs to draw. In the OnRender method, it draws each slice to the DrawingContext object.

The code for the PieChartControl is as follows:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
namespace Recipe_04_11
{
   public class PieChartControl : FrameworkElement
   {
       #region Slices

       public List<double> Slices
       {
           get
           {
               return (List<double>) GetValue(SlicesProperty);
           }
           set
           {
               SetValue(SlicesProperty, value);
           }
       }

       // Using a DependencyProperty as the backing store for slices.
       // This enables animation, styling, binding, etc...
       public static readonly DependencyProperty SlicesProperty =
           DependencyProperty.Register("Slices",
                                       typeof(List<double>),
                                       typeof(PieChartControl),
                                       new FrameworkPropertyMetadata(
                                           null,
                                           FrameworkPropertyMetadataOptions.
                                           AffectsRender,
                                           new PropertyChangedCallback(
                                                  OnPropertyChanged)));
       #endregion

       /// <summary>
       /// Override the OnRender and draw the slices
       /// for the pie chart
       /// </summary>
       /// <param name="drawingContext"></param>
       protected override void OnRender(DrawingContext drawingContext)
       {
           List<double> segments = this.Slices;

           if(segments != null)
           {
               Size radius = new Size(
                   this.RenderSize.Width * 0.5,
                   this.RenderSize.Height * 0.5);
Point startPoint = new Point(radius.Width, 0);

         foreach(double slice in segments)
         {
              startPoint = DrawSlice(
                  drawingContext,
                  slice,
                  startPoint,
                  radius);
         }
      }
   }

   private Point DrawSlice(
                                      DrawingContext drawingContext,
                                      double slice,
                                      Point startPoint,
                                      Size radius)
   {
      // double theta = (slice.Percentage / 100) * 360;
      double theta = (slice / 100) * 360;

      // nb. This caters for the condition where we have one slice
      theta = (theta == 360) ? 359.99 : theta;

      //Note - we need to translate the point first.
      // Could be rolled into a single affine transformation.
      Point endPoint =
          RotatePoint(
              new Point(
                  startPoint.X - radius.Width,
                  startPoint.Y - radius.Height),
              theta);

      endPoint = new Point(
          endPoint.X + radius.Width,
          endPoint.Y + radius.Height);

      bool isLargeArc = (theta > 180);

      PathGeometry geometry = new PathGeometry();
      PathFigure figure = new PathFigure();

      geometry.Figures.Add(Figure);

      figure.IsClosed = true;
      figure.StartPoint = startPoint;
figure.Segments.Add(
                   new ArcSegment(endPoint, radius, 0, isLargeArc,
                                             SweepDirection.Clockwise, false));
       figure.Segments.Add(new LineSegment(startPoint, false));
       figure.Segments.Add(new LineSegment(endPoint, false));
       figure.Segments.Add(
                 new LineSegment(
                          new Point(
                               radius.Width, radius.Height), false));

       SolidColorBrush brush = new SolidColorBrush(GetRandomColor());
       drawingContext.DrawGeometry(brush, new Pen(brush, 1), geometry);

       startPoint = endPoint;
       return startPoint;
     }

     private const double _pi_by180 = Math.PI / 180;

     private Point RotatePoint(Point a, double phi)
     {
         double theta = phi * _pi_by180;
         double x = Math.Cos(theta) * a.X + -Math.Sin(theta) * a.Y;
         double y = Math.Sin(theta) * a.X + Math.Cos(theta) * a.Y;
         return new Point(x, y);
     }

     protected static void OnPropertyChanged(
                                      DependencyObject o,
                                      DependencyPropertyChangedEventArgs args)
     {
         PieChartControl pcc = o as PieChartControl;
         if(null != pcc)
             pcc.InvalidateVisual();
     }

     private static Random seed = new Random();

     private Color GetRandomColor()
     {
         Color newColor = new Color();

         newColor.A = (byte) 255;
         newColor.R = (byte) seed.Next(0, 256);
         newColor.G = (byte) seed.Next(0, 256);
         newColor.B = (byte) seed.Next(0, 256);
return newColor;
      }
   }
}

The following XAML defines a window displaying three PieChartControl controls.

<Window x:Class="Recipe_04_11.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_11="clr-namespace:Recipe_04_11;assembly="
    Title="WPF Recipes 4_11" Height="120" Width="180">
    <Grid>

        <StackPanel
            Orientation="Horizontal"
            Margin="4">

            <Recipe_04_11:PieChartControl
            x:Name="pieChart1"
            Width="36"
            Height="36"
            Margin="8"
           />
            <Recipe_04_11:PieChartControl
            x:Name="pieChart2"
            Width="36"
            Height="36"
            Margin="8"
           />
            <Recipe_04_11:PieChartControl
            x:Name="pieChart3"
            Width="36"
            Height="36"
            Margin="8"
           />
        </StackPanel>

    </Grid>
</Window>

In the constructor for the window, the pie charts are given their slices:

using System.Collections.Generic;
using System.Windows;

namespace Recipe_04_11
{
    public partial class Window1 : Window
{
     public Window1()
     {
         InitializeComponent();

         // Set up the slices
         pieChart1.Slices = new List <double>();
         pieChart1.Slices.Add(30);
         pieChart1.Slices.Add(60);
         pieChart1.Slices.Add(160);

         pieChart2.Slices = new List <double>();
         pieChart2.Slices.Add(30);
         pieChart2.Slices.Add(90);

         pieChart3.Slices = new List <double>();
         pieChart3.Slices.Add(90);
         pieChart3.Slices.Add(180);
      }
   }
}

Figure 4-9 shows the resulting window.

Creating a custom-drawn PieChartControl

Figure 4-9. Creating a custom-drawn PieChartControl

Create a Numeric TextBox Control

Problem

You need a System.Windows.Controls.TextBox control that accepts only numeric values as the input.

Solution

Create a control that inherits from TextBox, add a System.Windows.DependencyProperty to the code-behind called Number, and specify a type of double.

Override the OnPreviewTextInput method of the TextBox control, and if the text that is being input cannot be parsed to a double, set the Handled property of the System.Windows.Input.TextCompositionEventArgs to True.

Add code to ensure that when the Number property is changed, the Text property is also changed, and vice versa.

Tip

If you need the TextBox to contain only integer values, then simply change the type of Number property to int, and use int.TryParse instead of double.TryParse to check whether a new text input should be allowed.

How It Works

By inheriting from a TextBox control, you get a custom control with all the behavior and appearance properties of a TextBox, but you also get the ability to override and modify certain aspects or features for the specific needs of a situation.

In this case, you override the OnPreviewTextInput method to intercept the inputting of text into the base TextBox and allow text to be input only if it can be parsed to a double. This is possible because the TextCompositionEventArgs class has a Handled property, and the text is input only if this property is not set to False.

By using DependencyProperty to store the numeric value of the Text, you can use the full range of WPF features such as data binding, styles, and animations to interact with the control.

The Code

The following code-behind shows a class called NumericTextboxControl that inherits from the TextBox class and adds a Number property of type double, backed by a DependencyProperty. There is code in the OnPreviewTextInput method to allow only text that can be converted to a double as input. Then, there is code in the OnNumberChanged and OnTextChanged methods to ensure that the values in the Text and Number properties are synchronized.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Recipe_04_12
{
    public partial class NumericTextboxControl : TextBox
    {
        // Flag is True if the Text property is changed
        private bool isTextChanged = false;

        // Flag is True if the Number property is changed
        private bool isNumberChanged = false;
public NumericTextboxControl()
     {
        InitializeComponent();
     }

     /// <summary>
     /// Public property to store the numeric
     /// value of the control's Text property
     /// </summary>
     public double Number
     {
         get
         {
            return (double) GetValue(NumberProperty);
         }

         set
         {
            SetValue(NumberProperty, value);
         }
     }

     public static readonly DependencyProperty NumberProperty =
         DependencyProperty.Register("Number",
                                     typeof(double),
                                     typeof(NumericTextboxControl),
                                     new PropertyMetadata(
                                         new PropertyChangedCallback(
                                                   OnNumberChanged)));

     private static void OnNumberChanged(
                                            DependencyObject sender,
                                            DependencyPropertyChangedEventArgs e)
     {
       NumericTextboxControl control = (NumericTextboxControl) sender;

       if(!control.isTextChanged)
       {
           // Number property has been changed from the outside,
           // via a binding or control, so set the Text property
           control.isNumberChanged = true;
           control.Text = control.Number.ToString();
           control.isNumberChanged = false;
       }
    }
protected override void OnTextChanged(TextChangedEventArgs e)
     {
        if(!isNumberChanged)
        {
            // Text property has been changed from
            // text input, so set the Number property
            // nb. It will default to 0 if the text
            // is empty or "-"
            isTextChanged = true;
            double number;
            double.TryParse(this.Text, out number);
            this.Number = number;
            isTextChanged = false;
        }

        base.OnTextChanged(e);
     }

     protected override void OnPreviewTextInput(TextCompositionEventArgs e)
     {
         // Get the preview of the new text
         string newTextPreview =
             this.Text.Insert(
                 this.SelectionStart,
                 e.Text);

         // Try to parse it to a double
         double number;
         if(!double.TryParse(newTextPreview, out number)
            && newTextPreview != "-")
         {
             // Mark the event as being handled if
             // the new text can't be parsed to a double
             e.Handled = true;
         }

         base.OnPreviewTextInput(e);
     }
   }
}

The following XAML shows how to use a NumericTextBoxControl in a window. There is also a button to demonstrate changing the value of the Number property and updating the text, as well as a System.Windows.Controls.TextBlock control that demonstrates binding to the Number property.

<Window x:Class="Recipe_04_12.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_12="clr-namespace:Recipe_04_12;assembly="
    Title="WPF Recipes 4_12" Height="120" Width="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.5*"/>
            <ColumnDefinition Width="0.5*"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>

        <TextBlock
            Grid.Column="0"
            VerticalAlignment="Center"
            HorizontalAlignment="Center">
                Only accepts numbers:
         </TextBlock>

        <Recipe_04_12:NumericTextboxControl
            x:Name="numTextBox"
            Width="80"
            Height="20"
            Grid.Column="1"
            VerticalAlignment="Center"
            HorizontalAlignment="Center"
            />

        <Button Click="Button_Click"
                Grid.Row="1"
                Grid.Column="0"
                VerticalAlignment="Center"
                Width="120"
                Height="24" >
            Increment the number
        </Button>

        <StackPanel
            Orientation="Horizontal"
            VerticalAlignment="Center"
            Grid.Row="1"
            Grid.Column="1">
            <TextBlock>The number is:</TextBlock>
<TextBlock
          Margin="4,0,0,0"
          Text="{Binding
          ElementName=numTextBox,
          Path=Number,
          UpdateSourceTrigger=PropertyChanged}"
            />
    </StackPanel>

    </Grid>
</Window>

Create a Scrollable Canvas Control

Problem

You need to provide scroll functionality in a System.Windows.Controls.Canvas control.

Solution

Create a class that derives from System.Windows.Controls.Canvas, and override the MeasureOverride method. In this method, iterate through the FrameworkElements in the Children collection, call the Measure method of each child, and determine the highest value for the Top and Left properties. Then return these as the dimensions the canvas should occupy based on the layout of its children.

This new Canvas control can be wrapped in a System.Windows.Controls.ScrollViewer, and if the Canvas contains child elements that require scrolling in order to be brought into view, the ScrollViewer now provides the correct scroll amount.

Warning

The default value for the VerticalScrollBarVisibility property of a ScrollViewer is System.Windows.Controls.ScrollBarVisibility.Visible, but the default value for the HorizontalScrollBarVisibility property is Hidden. So if this property is not explicitly changed to Visible, the ScrollViewer will not show the horizontal scrollbar regardless of whether there are any child elements out of view and to the right.

How It Works

By default, a Canvas has no height or width. This causes issues if you want to use a Canvas in a ScrollViewer because the ScrollViewer doesn't ever see the Canvas spill out of its viewport. When overriding the Canvas's MeasureOverride method, you can determine a bounding rectangle for all the child items in the Canvas and return this as the Canvas's size. Then if the Canvas contains child elements that require scrolling to be brought into view, the ScrollViewer provides the correct scroll amount.

The Code

The following example demonstrates how to create a ScrollableCanvasControl, a custom reusable canvas control that can be wrapped in a ScrollViewer and display horizontal and vertical scrollbars to scroll the child items into view. This user control is then used in a window, which is shown in Figure 4-10.

The code for the ScrollableCanvasControl is as follows:

using System;
using System.Windows;
using System.Windows.Controls;

namespace Recipe_04_13
{
    public class ScrollableCanvasControl : Canvas
    {
        static ScrollableCanvasControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
            typeof(ScrollableCanvasControl),
            new FrameworkPropertyMetadata(
                typeof(ScrollableCanvasControl)));
        }

        protected override Size MeasureOverride(
            Size constraint)
        {
            double bottomMost = 0d;
            double rightMost = 0d;

            // Loop through the child FrameworkElements,
            // and track the highest Top and Left value
            // amongst them.
            foreach(object obj in Children)
            {
                FrameworkElement child = obj as FrameworkElement;

                if(child != null)
                {
                    child.Measure(constraint);
bottomMost = Math.Max(
               bottomMost,
               GetTop(child) +
               child.DesiredSize.Height);
           rightMost = Math.Max(
               rightMost,
               GetLeft(child) +
               child.DesiredSize.Width);
         }
       }

       if(double.IsNaN(bottomMost)
          || double.IsInfinity(bottomMost))
       {
           bottomMost = 0d;
       }

       if(double.IsNaN(rightMost)
          || double.IsInfinity(rightMost))
       {
          rightMost = 0d;
       }

       // Return the new size
       return new Size(rightMost, bottomMost);
     }
   }
}

The following XAML defines a window with two ScrollView controls side by side. The one on the left contains a normal Canvas, which in turn contains two System.Windows.Controls.Button controls. One of the buttons is positioned in the view; one is positioned below the bottom of the window. Because this is a normal Canvas, the second button is not displayed and cannot be scrolled into view. The Canvas on the right is a ScrollableCanvasControl containing identical buttons. However, this time, the vertical scrollbar is displayed, and the bottom button can be scrolled into view. Figure 4-10 shows the results.

<Window x:Class="Recipe_04_13.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_13="clr-namespace:Recipe_04_13;assembly="
    Title="WPF Recipes 4_13" Height="200" Width="400" >
<Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Width" Value="Auto" />
            <Setter Property="Height" Value="24" />
        </Style>
    </Window.Resources>

    <Grid>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.5*"/>
            <ColumnDefinition Width="0.5*"/>
        </Grid.ColumnDefinitions>

        <ScrollViewer Grid.Column="0">
            <Canvas>
                <Button
                    Canvas.Top="80"
                    Canvas.Left="80">
                    In View
                </Button>
                <Button
                    Canvas.Top="300"
                    Canvas.Left="80">
                    Out of view
                </Button>
            </Canvas>
        </ScrollViewer>

        <ScrollViewer Grid.Column="1">
            <Recipe_04_13:ScrollableCanvasControl>
                <Button
                    Canvas.Top="80"
                    Canvas.Left="80">
                    In View
                </Button>
                <Button
                    Canvas.Top="300"
                    Canvas.Left="80">
                    Out of View
                </Button>
            </Recipe_04_13:ScrollableCanvasControl>
        </ScrollViewer>

    </Grid>
</Window>
Creating a scrollable Canvas control

Figure 4-10. Creating a scrollable Canvas control

Create a Zoomable Canvas Control

Problem

You need to provide zoom functionality in a System.Windows.Controls.Canvas control.

Solution

Create a class that derives from System.Windows.Controls.Canvas, and override the MeasureOverride method. In this method, iterate through the FrameworkElements in the Children collection, call the Measure method of each child, and determine the highest value for the Top and Left properties. Then return these as the dimensions the canvas should occupy based on the layout of its children.

This new Canvas control can be wrapped in a System.Windows.Controls.ScrollViewer, and if the Canvas contains child elements that require scrolling in order to be brought into view, the ScrollViewer now provides the correct scroll amount.

Set the System.Windows.Media.Transform.LayoutTransform property of the Canvas to a System.Windows.Media.ScaleTransform, and bind the ScaleX and ScaleY properties of the ScaleTransform to the Value property of a System.Windows.Controls.Slider control.

How It Works

In overriding the Canvas's MeasureOverride method, you can determine a bounding rectangle for all the child items in the Canvas and return this as the Canvas's size. Then if the Canvas contains child elements that require scrolling to be brought into view, the ScrollViewer provides the correct scroll amount.

By setting the LayoutTransform property of the Canvas to a ScaleTransform, you can automatically transform the scale of the Canvas control by a factor provided by the value of a Slider control.

The Code

The following example demonstrates how to create a ZoomableCanvasControl, which is a custom reusable Canvas control that can be wrapped in a ScrollViewer, and it sets the LayoutTransform property of the Canvas to a ScaleTransform.

This user control is then used in a window that contains a Slider control, and it binds the ScaleX and ScaleY properties of the ScaleTransform to the value of the Slider control.

Figure 4-11 shows the resulting window. As the Slider moves left and right, the contents of the Canvas zoom in and out.

The code for the ZoomableCanvasControl is as follows:

using System;
using System.Windows;
using System.Windows.Controls;

namespace Recipe_04_14
{
    public class ZoomableCanvasControl : Canvas
    {
        static ZoomableCanvasControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(ZoomableCanvasControl),
                new FrameworkPropertyMetadata(
                    typeof(ZoomableCanvasControl)));
        }

        protected override Size MeasureOverride(
            Size constraint)
        {
            double bottomMost = 0d;
            double rightMost = 0d;

            // Loop through the child FrameworkElements,
            // and track the highest Top and Left value
            // amongst them.
            foreach(object obj in Children)
            {
                FrameworkElement child = obj as FrameworkElement;

                if(child != null)
                {
                    child.Measure(constraint);

                    bottomMost = Math.Max(
                        bottomMost,
                        GetTop(child) +
                        child.DesiredSize.Height);
rightMost = Math.Max(
               rightMost,
               GetLeft(child) +
               child.DesiredSize.Width);
           }
       }

       if(double.IsNaN(bottomMost)
          || double.IsInfinity(bottomMost))
       {
          bottomMost = 0d;
       }

       if(double.IsNaN(rightMost)
          || double.IsInfinity(rightMost))
       {
          rightMost = 0d;
       }

       // Return the new size
       return new Size(rightMost, bottomMost);
     }
   }
}

The following XAML defines a window with a ZoomableCanvasControl inside a ScrollViewer control. There is a Slider control docked to the bottom of the window, whose Value property is bound to the ScaleX and ScaleY properties of a ScaleTransform within the ZoomableCanvasControl.

<Window x:Class="Recipe_04_14.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_14="clr-namespace:Recipe_04_14;assembly="
   Title="WPF Recipes 4_14" Height="300" Width="300" >

    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Width" Value="Auto" />
            <Setter Property="Height" Value="24" />
        </Style>
   </Window.Resources>

   <DockPanel>
<Slider
             DockPanel.Dock="Bottom"
             x:Name="zoomSlider"
             Minimum="0.1"
             Maximum="5"
             Value="1"
            />

        <ScrollViewer
            VerticalScrollBarVisibility="Auto"
            HorizontalScrollBarVisibility="Auto">

            <Recipe_04_14:ZoomableCanvasControl x:Name="zoomControl">
                <Canvas.LayoutTransform>
                    <ScaleTransform
                        ScaleX="{Binding Path=Value, ElementName=zoomSlider}"
                        ScaleY="{Binding Path=Value, ElementName=zoomSlider}"
                        />
                </Canvas.LayoutTransform>
                <Rectangle
                    Canvas.Top="0"
                    Canvas.Left="0"
                    StrokeThickness="2"
                    Stroke="Red"
                    Width="50"
                    Height="50"
                    />
                <Rectangle
                    Canvas.Top="50"
                    Canvas.Left="50"
                    StrokeThickness="2"
                    Stroke="Blue"
                    Width="150"
                    Height="150"
                    />
                <Rectangle
                    Canvas.Top="200"
                    Canvas.Left="200"
                    StrokeThickness="2"
                    Stroke="Green"
                    Width="200"
                    Height="200"
                    />
            </Recipe_04_14:ZoomableCanvasControl>
        </ScrollViewer>

    </DockPanel>
</Window>

Figure 4-11 shows the resulting window.

Creating a zoomable Canvas control

Figure 4-11. Creating a zoomable Canvas control

Create a Drag Canvas Control

Problem

You need to be able to drag elements inside your System.Windows.Controls.Canvas control.

Solution

Create a class that derives from System.Windows.Controls.Canvas, and override the OnPreviewMouseLeftButtonDown, OnPreviewMouseLeftButtonUp, and OnPreviewMouseMove methods. Add logic to these methods to determine when the left mouse button is pressed down on an element within the Canvas and where the element should be moved to if the mouse is moved before the button is released.

How It Works

In the Canvas control's OnPreviewMouseLeftButtonDown method, store the current mouse position and the current position of the selected UI element on the Canvas. In the OnPreviewMouseMove method, get the new position of the mouse, and use it to calculate the desired position of the UI element. Call the Canvas.SetLeft and Canvas.SetTop methods to set this position.

The Code

The following example demonstrates how to create a DragCanvasControl, a custom reusable Canvas control that overrides the OnPreviewMouseLeftButtonDown, OnPreviewMouseLeftButtonUp, and OnPreviewMouseMove methods to automatically drag elements inside the Canvas when the left mouse button is pressed down and the mouse is moved.

Figure 4-12 shows the resulting window. The shape elements in the canvas can be dragged around the Canvas by clicking them.

The code for the DragCanvasControl is as follows:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Recipe_04_15
{
    public class DragCanvasControl : Canvas
    {
        private Point startPoint;
        private Point selectedElementOrigins;
        private bool isDragging;
        private UIElement selectedElement;

        static DragCanvasControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(DragCanvasControl),
                new FrameworkPropertyMetadata(
                    typeof(DragCanvasControl)));
       }

       protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
       {
           base.OnPreviewMouseLeftButtonDown(e);

           if(e.Source != this)
           {
               if(!isDragging)
               {
                   startPoint = e.GetPosition(this);
                   selectedElement = e.Source as UIElement;
                   this.CaptureMouse();

                   isDragging = true;

                   selectedElementOrigins =
                       new Point(
                           Canvas.GetLeft(selectedElement),
                           Canvas.GetTop(selectedElement));
               }
               e.Handled = true;
           }
        }
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
     {
        base.OnPreviewMouseLeftButtonUp(e);

        if(this.IsMouseCaptured)
        {
            isDragging = false;
            this.ReleaseMouseCapture();

            e.Handled = true;
        }
     }

     protected override void OnPreviewMouseMove(MouseEventArgs e)
     {
         base.OnPreviewMouseMove(e);

         if(this.IsMouseCaptured)
         {
             if(isDragging)
             {
                 Point currentPosition = e.GetPosition(this);

                 double elementLeft = (currentPosition.X - startPoint.X) +
                                      selectedElementOrigins.X;
                 double elementTop = (currentPosition.Y - startPoint.Y) +
                                     selectedElementOrigins.Y;

                 Canvas.SetLeft(selectedElement, elementLeft);
                 Canvas.SetTop(selectedElement, elementTop);
            }
         }
      }
   }
}

The following XAML defines a window with a DragCanvasControl containing three shape elements:

<Window x:Class="Recipe_04_15.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Recipe_04_15="clr-namespace:Recipe_04_15;assembly="
    Title="WPF Recipes 4_15" Height="200" Width="300">
<Recipe_04_15:DragCanvasControl>

     <Rectangle
         Canvas.Top="8"
         Canvas.Left="8"
         Width="32"
         Height="32"
         Fill="Blue"
         />

     <Ellipse
         Canvas.Top="36"
         Canvas.Left="48"
         Width="40"
         Height="24"
         Fill="Yellow" />

     <Ellipse
         Canvas.Top="60"
         Canvas.Left="96"
         Width="32"
         Height="32"
         Fill="Red" />

    </Recipe_04_15:DragCanvasControl>

</Window>

Figure 4-12 shows the resulting window.

Creating a draggable Canvas control

Figure 4-12. Creating a draggable Canvas control

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset