Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating and consuming a custom WPF control

0.00/5 (No votes)
1 Mar 2007 1  
Creating and consuming a custom WPF control

Introduction

Whenever a new technology comes along, I personally find that the best way to get to grips with its functionality is to try and create something you have done in another language. To this end, this article will describe how to create a custom control in WPF which will raise custom events. The custom control will then be placed with a standard XAML window and the custom control's events shall be subscribed to. That's it in a nutshell. But along the way there are several things that I would like to point out to you.

The proposed structure will be as follows:

  • A word on XAML / WPF
  • The custom Control Itself
  • A word on events in .NET 3.0
  • Referencing an external custom control in an XAML window
  • Where is the InitializeComponent() method anyway
  • A screen shot of the demo app

A Word On XAML / WPF

WPF applications are quite similar is one sense to ASP.NET applications; there may (or may not) be an XAML file, and also a code behind file where the XAML file contains the windows / control rendering, and the code behind does all the procedural code. This is one development model. But there is another way. Anything that can be done in XAML can also be done entirely in code behind (C#/ VB). To this end the custom control that I've created is entirely code created. As for this example it just seemed to make more sense.

The Custom Control Itself

As color pickers seem to be almost universally popular at codeproject, I thought let's do one of them. This is a single control that has been created in a separate Visual Studio 2005 project, and is part of the whole solution. I have done this as it is the most common way that we all use third party controls. We get a DLL and make a reference to it. In fact I have chosen this path, as the XAML directives to reference a control do vary slightly depending on whether it is an internal class, or an external DLL. Most commonly I thought it would be a third party external DLL that was being referenced. If you don't get this, don't worry. There will be more on it later.

So without further ado, let's look at the code:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace ColorPicker
{
    #region ColorPickerControl CLASS
    /// <summary>

    /// A simple color picker control, with a custom event that uses

    /// the standard RoutedEventArgs.

    /// <br/>

    /// NOTE: I also tried to create a custom event with custom inherited

    /// RoutedEventArgs, but this didn't seem to work, 

    /// so this event is commented out. But if anyone knows how to do this 

    /// please let me know, as far as I know

    /// I am doing everything correctly

    /// </summary>

    public class ColorPickerControl : ListBox
    {
        #region InstanceFields
        //A RoutedEvent using standard RoutedEventArgs, event declaration

        //The actual event routing

        public static readonly RoutedEvent NewColorEvent =
            EventManager.RegisterRoutedEvent
        ("NewColor", RoutingStrategy.Bubble,
                   typeof(RoutedEventHandler), typeof(ColorPickerControl));

        //A RoutedEvent using standard custom ColorRoutedEventArgs, 

        //event declaration


        ////the event handler delegate

        public delegate void NewColorCustomEventHandler
            (object sender, ColorRoutedEventArgs e);

        ////The actual event routing

        public static readonly RoutedEvent NewColorCustomEvent =
             EventManager.RegisterRoutedEvent
        ("NewColorCustom", RoutingStrategy.Bubble,
                   typeof(NewColorCustomEventHandler), 
        typeof(ColorPickerControl));
        //******************************************************************

        //string array or colors

        private string[] _sColors =
        {
            "Black", "Brown", "DarkGreen", "MidnightBlue",
                "Navy", "DarkBlue", "Indigo", "DimGray",
            "DarkRed", "OrangeRed", "Olive", "Green",
                "Teal", "Blue", "SlateGray", "Gray",
            "Red", "Orange", "YellowGreen", "SeaGreen",
                "Aqua", "LightBlue", "Violet", "DarkGray",
            "Pink", "Gold", "Yellow", "Lime",
                "Turquoise", "SkyBlue", "Plum", "LightGray",
            "LightPink", "Tan", "LightYellow", "LightGreen",
                "LightCyan", "LightSkyBlue", "Lavender", "White"
        };
        #endregion
        #region Constructor
        /// <summary>

        /// Constructor for ColorPickerControl, which is a ListBox subclass

        /// </summary>

        public ColorPickerControl()
        {
            // Define a template for the Items, 

            // used the lazy FrameworkElementFactory method

            FrameworkElementFactory fGrid = new
                FrameworkElementFactory
        (typeof(System.Windows.Controls.Primitives.UniformGrid));
            fGrid.SetValue
       (System.Windows.Controls.Primitives.UniformGrid.ColumnsProperty,10);
            // update the ListBox ItemsPanel with the new 

            // ItemsPanelTemplate just created

            ItemsPanel = new ItemsPanelTemplate(fGrid);

            // Create individual items

            foreach (string clr in _sColors)
            {
                // Creat bounding rectangle for items data

                Rectangle rItem = new Rectangle();
                rItem.Width = 10;
                rItem.Height = 10;
                rItem.Margin = new Thickness(1);
                rItem.Fill = 
            (Brush)typeof(Brushes).GetProperty(clr).GetValue(null, null);
                //add rectangle to ListBox Items

                Items.Add(rItem);

                //add a tooltip

                ToolTip t = new ToolTip();
                t.Content = clr;
                rItem.ToolTip = t;
            }
            //Indicate that SelectedValue is Fill property of Rectangle item.

            //Kind of like an XPath query, 

            //this is the string name of the property

            //to use as the selected item value from the actual item data. 

            //The item data being a Rectangle in this case

            SelectedValuePath = "Fill";
        }
        #endregion
        #region Events
        // Provide CLR accessors for the event

        public event RoutedEventHandler NewColor
        {
            add { AddHandler(NewColorEvent, value); }
            remove { RemoveHandler(NewColorEvent, value); }
        }

        // This method raises the NewColor event

        private void RaiseNewColorEvent()
        {
            RoutedEventArgs newEventArgs = new RoutedEventArgs(NewColorEvent);
            RaiseEvent(newEventArgs);
        }

        // Provide CLR accessors for the event

        public event NewColorCustomEventHandler NewColorCustom
        {
            add { AddHandler(NewColorCustomEvent, value); }
            remove { RemoveHandler(NewColorCustomEvent, value); }
        }

        // This method raises the NewColorCustom event

        private void RaiseNewColorCustomEvent()
        {
            ToolTip t = (ToolTip)(SelectedItem as Rectangle).ToolTip;
            ColorRoutedEventArgs newEventArgs = 
        new ColorRoutedEventArgs(t.Content.ToString());
            newEventArgs.RoutedEvent = ColorPickerControl.NewColorCustomEvent;
            RaiseEvent(newEventArgs);
        }
        //*******************************************************************

        #endregion
        #region Overrides
        /// <summary>

        /// Overrides the OnSelectionChanged ListBox inherited method, and

        /// raises the NewColorEvent

        /// </summary>

        /// <param name="e">the event args</param>

        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            //raise the event with standard RoutedEventArgs event args

            RaiseNewColorEvent();
            //raise the event with the custom ColorRoutedEventArgs event args

            RaiseNewColorCustomEvent();
            //****************************************************************

        }
        #endregion
    }
    #endregion
    #region ColorRoutedEventArgs CLASS
    /// <summary>

    /// ColorRoutedEventArgs : a custom event argument class

    /// </summary>

    public class ColorRoutedEventArgs : RoutedEventArgs
    {
        #region Instance fields
        private string _ColorName = "";
        #endregion
        #region Constructor
        /// <summary>

        /// Constructs a new ColorRoutedEventArgs object

        /// using the parameters provided

        /// </summary>

        /// <param name="clrName">the color name string</param>

        public ColorRoutedEventArgs(string clrName)
        {
            this._ColorName = clrName;
        }
        #endregion
        #region Public properties
        /// <summary>

        /// Gets the stored color name

        /// </summary>

        public string ColorName
        {
            get { return _ColorName; }
        }
        #endregion
    }
    #endregion
}

It can be seen that this is all fairly normal C# .NET 3.0 code (that is if you are OK with .NET 3.0 stuff, I am just learning). I want to pay some special attention to the constructor. Let's have a look at that part by part.

// Define a template for the Items, use the lazy FrameworkElementFactory

// method

FrameworkElementFactory fGrid = new FrameworkElementFactory
    (typeof(System.Windows.Controls.Primitives.UniformGrid));
    fGrid.SetValue
    (System.Windows.Controls.Primitives.UniformGrid.ColumnsProperty, 10);
//update the ListBox ItemsPanel with the new ItemsPanelTemplate just created

ItemsPanel = new ItemsPanelTemplate(fGrid);

The FrameworkElementFactory class is a way to programmatically create templates, which are subclasses of FrameworkTemplate such as ControlTemplate or DataTemplate. This is equivalent to creating a <ControlTemplate> tag in XAML markup. So what we are really doing here is saying that the internal inherited Listbox.ItemsPanel will have a template applied to it that will be a uniform grid layout with 10 columns.

// Create individual items

foreach (string clr in _sColors)
{
    // Create bounding rectangle for items data

    Rectangle rItem = new Rectangle();
    rItem.Width = 10;
    rItem.Height = 10;
    rItem.Margin = new Thickness(1);
    rItem.Fill = 
    (Brush)typeof(Brushes).GetProperty(clr).GetValue(null, null);
    //add rectangle to ListBox Items

    Items.Add(rItem);
    //add a tooltip

    ToolTip t = new ToolTip();
    t.Content = clr;
    rItem.ToolTip = t;
}

This section of the code is responsible for creating the individual ListItem contents. So what is going on? Well, the items are being created as Rectangle objects (yep that's right, Rectangles). Then the Rectangles are being filled with a Brush color, and then the Rectangle has a ToolTip applied.

//Indicate that SelectedValue is Fill property of Rectangle item.

//Kind of like an XPath query, this is the string name of the property

//to use as the selected item value from the actual item data. The item

//data being a Rectangle in this case

SelectedValuePath = "Fill";

Finally the SelectedValuePath is told that the property that should be mapped to the SelectedValue is "Fill". So this means that whenever we get the SelectedValue the object it will be is a Brush as default, as "Fill" is a Brush Type, unless it is cast to another object Type. Isn't that mental. WPF is mind blowing, it really is.

The more eagle eyed amongst you will notice that the code for the control contains 2 events one of which is commented out. More on this later.

A word on events in .NET 3.0

Microsoft being Microsoft didn't want us to get too comfortable with things, so they have overhauled everything it would appear. Even something as small as events, is no longer the same as it was in .NET 2.0.

The code snippets below represent the new .NET 3.0 way of creating events.

I have created a custom event called NewColorEvent so let's have a look at how to define an event:

//A RoutedEvent using standard RoutedEventArgs, event declaration

//The actual event routing

public static readonly RoutedEvent NewColorEvent =
          EventManager.RegisterRoutedEvent("NewColor", RoutingStrategy.Bubble,
            typeof(RoutedEventHandler), typeof(ColorPickerControl));

What else do we need to do, well we need to create the accessors for subscribing / unsubscribing to the event.

// Provide CLR accessors for the event

public event RoutedEventHandler NewColor
{
    add { AddHandler(NewColorEvent, value); }
    remove { RemoveHandler(NewColorEvent, value); }
}

And we also need a raise event method such as:

// This method raises the NewColor event

private void RaiseNewColorEvent()
{
     RoutedEventArgs newEventArgs = new RoutedEventArgs(NewColorEvent);
     RaiseEvent(newEventArgs);
}

And lastly we need to raise the event somewhere. I have chosen to do this in an override of the inherited ListBox OnSelectionChanged method; this is shown below:

//raise the event with standard RoutedEventArgs event args

RaiseNewColorEvent();

And that's all there is to creating a custom event in a custom control. We just need to place the control somewhere now and subscribe to this lovely new event.

Referencing an external custom control in an XAML window

OK so you think you know how to make a reference to a DLL which contains a custom control. You just create a new tab on the toolbar, and browse to the DLL and any of the contained controls to the toolbar. Right. Well that didn't seem to work. So what do you have to do. Is Add a project reference (right click on references) and browse to the assembly (DLL) with the custom control(s), only one in this articles case.

So that's step 1. Then we actually want to use the custom control within a XAML window. So we have to add an extra directive to the XAML Windows root element. The important part to add is as follows:

If using a code file that you have the source code for

xmlns:src="clr-namespace:NAMESPACE_NEEDED"

If using a external DLL

xmlns:src="clr-namespace:NAMESPACE_NEEDED;assembly=ASSEMBLYNAME_NEEDED"

So for the attached example, where we have an external DLL which has a control we need to use, the root element would be changed to the following:

<Window x:Class="ColorControlApp.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:src="clr-namespace:ColorPicker;assembly=ColorPickerControl"
    Title="ColorControlApp" Height="300" Width="300">

That's part of the story. We have now successfully referenced the external user control in the XAML, but we still don't have an instance of the control in the markup yet. So how do we do that. Well we do something like this:

<src:ColorPickerControl HorizontalAlignment="Center"
                        VerticalAlignment="Center" Name="lstColorPicker"/>

OK so now we have done the XAML part, but what about the code behind file. We still need to do that part. So how is that done. Well, luckily that part is easier. Its just a normal using statement we need:

using ColorPicker;

Where is the InitializeComponent() method anyway ?

Now we really do have a fully referenced external DLL, which contains a user control, which we now have an instance of within our XAML window, that is also now known about by the code behind logic because of the two steps just carried out.

But just how does the XAMLs code behind file know about the user control contained within the XAML file. Well the answer to that, is that when Visual Studio compiles a project, it creates a new generated source file, which is placed into the DEBUG\OBJ or RELEASE\OBJ (depends on how you are compiling the project). This file is called the same as the current XAML window file, but the extension will be g.cs for c# or g.vb for VB.

The following screen print shows this for the attached project:

Can you see there is a Window1.g.cs (as I use C#) file there.

So what is that all about. Well let's have a look.

//---------------------------------------------------------------------------

// <auto-generated>

//     This code was generated by a tool.

//     Runtime Version:2.0.50727.42

//

//     Changes to this file may cause incorrect behavior and will be lost if

//     the code is regenerated.

// </auto-generated>

//---------------------------------------------------------------------------


using ColorPicker;
using System;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Media.TextFormatting;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace ColorControlApp 
{
    /// <summary>

    /// Window1

    /// </summary>

    public partial class Window1 : 
    System.Windows.Window, System.Windows.Markup.IComponentConnector 
    {
        internal System.Windows.Controls.StackPanel Stack;
        internal System.Windows.Controls.Label lblColor;
        internal ColorPicker.ColorPickerControl lstColorPicker;


        private bool _contentLoaded;
        /// <summary>

        /// InitializeComponent

        /// </summary>

        [System.Diagnostics.DebuggerNonUserCodeAttribute()]
        public void InitializeComponent() 
        {
            if (_contentLoaded) 
            {
                return;
            }
            _contentLoaded = true;
            System.Uri resourceLocater = 
            new System.Uri("/ColorControlApp;component/window1.xaml", 
            System.UriKind.Relative);
            System.Windows.Application.LoadComponent(this, resourceLocater);
        }

        [System.Diagnostics.DebuggerNonUserCodeAttribute()]
        [System.ComponentModel.EditorBrowsableAttribute
        (System.ComponentModel.EditorBrowsableState.Never)]
        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute
        ("Microsoft.Design", 
        "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
        void System.Windows.Markup.IComponentConnector.Connect
                (int connectionId, object target) 
        {
            switch (connectionId)
            {
            case 1:
            this.Stack = ((System.Windows.Controls.StackPanel)(target));
            return;
            case 2:
            this.lblColor = ((System.Windows.Controls.Label)(target));
            return;
            case 3:
            this.lstColorPicker = ((ColorPicker.ColorPickerControl)(target));
            return;
            }
            this._contentLoaded = true;
        }
    }
}

It can be seen that this source file provides the missing parts, most noticeably the InitializeComponent() method and also notice that there are a few instance fields which represent the components within the XAML file. So this is how both the code behind and XAML files are compiled to form a single assembly with all the required information.

Demo Of The Attached Application

The last thing I should show is the running app. That part is probably not so important, as it is the concepts I was trying to share really.

But for completeness sake, here is a screen shot.

remember the ColorPicker is simply a specialized ListBox really. Quite impressive no?

That's it

Well although we've only created a simple control and used it within a single XAML page, I hope you can see that there are quite a few core concepts that were covered here.

So What Do You Think ?

I would just like to ask, if you liked the article please vote for it, as it lets me know if the article was at the right level or not.

Conclusion

I have quite enjoyed constructing this article. I hope you liked it. I think it will help you a lot when you get time to do some XAML / WPF type apps. I have only just started out with XAML and I truly believe it is set to totally change the sort of applications we are all going to see.

History

  • v1.1 01/03/07: Fixed the custom event with custom event args issue. Thanks very much to Steve Maier.
  • v1.0 01/03/07: Initial Issue

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here