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
public class ColorPickerControl : ListBox
{
#region InstanceFields
public static readonly RoutedEvent NewColorEvent =
EventManager.RegisterRoutedEvent
("NewColor", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ColorPickerControl));
public delegate void NewColorCustomEventHandler
(object sender, ColorRoutedEventArgs e);
public static readonly RoutedEvent NewColorCustomEvent =
EventManager.RegisterRoutedEvent
("NewColorCustom", RoutingStrategy.Bubble,
typeof(NewColorCustomEventHandler),
typeof(ColorPickerControl));
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
public ColorPickerControl()
{
FrameworkElementFactory fGrid = new
FrameworkElementFactory
(typeof(System.Windows.Controls.Primitives.UniformGrid));
fGrid.SetValue
(System.Windows.Controls.Primitives.UniformGrid.ColumnsProperty,10);
ItemsPanel = new ItemsPanelTemplate(fGrid);
foreach (string clr in _sColors)
{
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);
Items.Add(rItem);
ToolTip t = new ToolTip();
t.Content = clr;
rItem.ToolTip = t;
}
SelectedValuePath = "Fill";
}
#endregion
#region Events
public event RoutedEventHandler NewColor
{
add { AddHandler(NewColorEvent, value); }
remove { RemoveHandler(NewColorEvent, value); }
}
private void RaiseNewColorEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(NewColorEvent);
RaiseEvent(newEventArgs);
}
public event NewColorCustomEventHandler NewColorCustom
{
add { AddHandler(NewColorCustomEvent, value); }
remove { RemoveHandler(NewColorCustomEvent, value); }
}
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
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
RaiseNewColorEvent();
RaiseNewColorCustomEvent();
}
#endregion
}
#endregion
#region ColorRoutedEventArgs CLASS
public class ColorRoutedEventArgs : RoutedEventArgs
{
#region Instance fields
private string _ColorName = "";
#endregion
#region Constructor
public ColorRoutedEventArgs(string clrName)
{
this._ColorName = clrName;
}
#endregion
#region Public properties
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.
FrameworkElementFactory fGrid = new FrameworkElementFactory
(typeof(System.Windows.Controls.Primitives.UniformGrid));
fGrid.SetValue
(System.Windows.Controls.Primitives.UniformGrid.ColumnsProperty, 10);
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.
foreach (string clr in _sColors)
{
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);
Items.Add(rItem);
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.
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:
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.
public event RoutedEventHandler NewColor
{
add { AddHandler(NewColorEvent, value); }
remove { RemoveHandler(NewColorEvent, value); }
}
And we also need a raise event method such as:
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:
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.
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
{
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;
[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