Writing a Reusable Custom Control in WPF

In my previous post, I have already defined how you can inherit from an existing control and define your own reusable chunk. The reusable XAML code that I have defined there is actually a composition of one of more existing elements in a common design surface. But sometimes you must define  a new behaviour for your reusable component which does not belong to any of the already existing behaviors. Custom controls can help you in this. You can define a new behaviour for your Custom control which can have a default look and feel defined for it, and obviously which can be changed using Template for the user who is using the control. In this post I am going to provide you step by step approach on defining a Custom Control for your application.

Note: If you are really new to WPF, please read my WPF Tutorial before going further. 

Steps to Create a new Custom Control

Creating a custom control is very simple. Just in your project right click and add a new Item. In the dialog box that appears, choose Custom Control template and Name it. I call it here as SimpleControl.

Once you have added the control, it adds up a new class to your project which inherits from Control. The Template also provides you with some initial help to define your own control. Lets remove all the comments for now and start building a control.

Download Sample Code

In this control, I will show you all the components that you need for a Control so that it would help you guide each of the components individually.

Components that comprises a Custom Control

There are a number of components that you need to use inside your custom control which represents the behaviour for your control. Lets define each of them one by one before creating a Control.

1. Property  : Properties are one of the most important component for a control. In WPF, we use properties to define certain characteristics of a control. You can use either CLR properties or a Dependency Property. As you must know already a dependency property is a new Property System introduced with WPF, which holds a container of all the properties you define for a single instance of your control and which supports Property Value Inheritence, Animation, Templates, Styles, etc. Toread more about Dependency Property check my article.

Example :

public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color",
    typeof(Color),
    typeof(SimpleControl),
    new PropertyMetadata(Colors.Green));

public Color Color
{
    get
    {
        return (Color)this.GetValue(ColorProperty);
    }
    set
    {
        this.SetValue(ColorProperty, value);
    }
}

Here we have defined a new dependency Property called color, which will represent the initial BackColor of the control. The Default value for the Property is defined as Green.

2. Commands : It is important to define a command for your Control rather than using a method. Commands lets you use CommandBinding on a control just like your Button Class. The Command generally invoke certain set of action defined using ICommand interface. It also creates an Inversion of Control to invoke some user code as well, during the event.

Example :

static SimpleControl()
        {
            CommandManager.RegisterClassCommandBinding(typeof(SimpleControl),
                new CommandBinding(SimpleControl.CustomCommand, OnCustomCommand));
        }

        static void OnCustomCommand(object sender, ExecutedRoutedEventArgs e)
        {
            //Need to first retrieve the control
            SimpleControl invoker = sender as SimpleControl;

            //Do whatever you need
        }

        public static readonly ICommand CustomCommand = new RoutedUICommand("CustomCommand", "CustomCommand",
                                                        typeof(SimpleControl),
                                                        new InputGestureCollection(
                                                            new InputGesture[] {
                                                                new KeyGesture(Key.Enter),
                                                                new MouseGesture(MouseAction.LeftClick) }
                                                                )
                                                          );

Here I have defined a Command called CustomCommand. As you can notice I have used Static Constructor to register my command and static eventHandler so that it is more efficient. You can easily retrieve the actual object easily from sender.

3. RoutedEvents : Events are another most important thing for a control. You can define your own event for the control, but it should be RoutedEvent to ensure that WPF model supports them internally. RoutedEvent supports event routing, that the event can bubble or tunnel to the complete Visual Tree, when the event is raised. The FrameworkElement has a complete set of methods to use in defining a RoutedEvent.

Example :

public static readonly RoutedEvent InvertCallEvent = EventManager.RegisterRoutedEvent("InvertCall",
    RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SimpleControl));

public event RoutedEventHandler InvertCall
{
    add { AddHandler(InvertCallEvent, value); }
    remove { RemoveHandler(InvertCallEvent, value); }
}

private void OnInvertCall()
{
    RoutedEventArgs args = new RoutedEventArgs(InvertCallEvent);
    RaiseEvent(args);
}

Just like dependency property system, WPF event system also holds a collection of Events which appropriately raises the event for a particular control when some specific operation is executed. The WPF event system can take part on EventSetters, Triggers etc. You have noticed that you can use Addhandler and RemoveHandler from the Event Accessor to add or remove an event for a particular control. The RaiseEvent is used to raise an event for a particular control.

4. Handling an Event : Sometimes your control might require to handle an Event that already comes from Control. For instance, the Button class handles an event called MouseDown to create an event Click to it. In such a scenario you can use RegisterClassHandler method to handle an event.

Example :

static SimpleControl()
{
    EventManager.RegisterClassHandler(typeof(SimpleControl), Mouse.MouseDownEvent, new MouseButtonEventHandler(OnMouseDown));
}

static void OnMouseDown(object sender, MouseButtonEventArgs e)
{
    SimpleControl invoker = sender as SimpleControl;
    //Do handle event

    //Raise your event
    invoker.OnInvertCall();

    //Do Rest
}

Now here I have used RegisterClassHandler to handle the event. This will handle the MouseDown event for the control and invoke InvertCall event defined as RoutedEvent.

5. TemplatePart : For every control you should specify the rules how the UI for the control should look like. Template allows the user of your control to modify the look and feel, but using TemplatePart you can define the part of a control which you can change behaviour from within the control. Say for instance, if your control has a border, and your control wants to change its color when Mouse is hovered over the control, you can call the part attribute to apply this change which will be shown for any Template your user define later on, provided it should name the same part name.

Note: Currently PartNames are not actually checked for Templates, so you can omit a part
Example :

[TemplatePart(Name="PART_MainBorder", Type=typeof(Border))]
[TemplatePart(Name="PART_body", Type= typeof(ContentControl))]
public class SimpleControl : Control
{

    Border MainBorder;
    ContentControl Body;

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

        if (this.Template != null)
        {
            Border mainBorder = this.Template.FindName("PART_MainBorder", this) as Border;
            if (mainBorder != MainBorder)
            {
                //First unhook existing handler
                if (MainBorder != null)
                {
                    MainBorder.MouseEnter -= new MouseEventHandler(MainBorder_MouseEnter);
                    MainBorder.MouseLeave -= new MouseEventHandler(MainBorder_MouseLeave);
                }
                MainBorder = mainBorder;
                if (MainBorder != null)
                {
                    MainBorder.MouseEnter += new MouseEventHandler(MainBorder_MouseEnter);
                    MainBorder.MouseLeave += new MouseEventHandler(MainBorder_MouseLeave);
                }
            }

            Body = this.Template.FindName("PART_body", this) as ContentControl;

        }
    }

void MainBorder_MouseLeave(object sender, MouseEventArgs e)
{
    Border thisBorder = sender as Border;
    if (thisBorder != null)
    {
        thisBorder.Background = new SolidColorBrush(Colors.Red);
        if (Body != null)
        {
            Run r = new Run("Mouse Left!");
            r.Foreground = new SolidColorBrush(Colors.White);
            Body.Content = r;
        }
    }
}

void MainBorder_MouseEnter(object sender, MouseEventArgs e)
{
    Border thisBorder = sender as Border;
    if (thisBorder != null)
    {
        thisBorder.Background = new SolidColorBrush(Colors.Blue);
        if (Body != null)
        {
            Run r = new Run("Mouse Entered!");
            r.Foreground = new SolidColorBrush(Colors.White);
            Body.Content = r;
        }
    }
}

So here I have created two TemplatePart one named PART_MainBorder and another PART_body. In your Generic template you must define this TemplateParts so that the default control acts perfectly. The User can also redefine the template and once the user redefines it, it can also use these Part names to call a specific control inside, so that your control can apply something inside of it.  In this example I have used a border, which will change its color to Red when Mouse is entered inside of it, and to Blue when it is left.  You should note that I have used Template.FindName and passed the current instance of the control. This is important because Templates are generally shared by more than one instance.

When a Template is applied to a control it calls OnApplyTemplate. Make sure you are tolerant enough to handle bad templates, as you can see, I have checked every time if the object is available before doing anything with it as WPF ignores bad templates and your control might end up with a bad NullReferenceException somewhere if you dont gracefully eliminate the same.

Now in your actual Window, you should declare the template for the Control and define each of the PartNames you specify here.

6. Themes  : Yes your control should define a default Theme at least to make sure it at least appears when an object is created. At the basic level, when you add a new Custom Control to your project, you should add a style for Generic.XAML that is added up on the Themes folder. The AseemblyInfo also adds up a assembly level attribute which identifies where the default Theme template resides.

The Theme folder will by default hold a Generic.xaml. Lets add a default Style for our control.

<Style TargetType="{x:Type local:SimpleControl}">
    <Setter Property="HorizontalAlignment" Value="Center" />
    <Setter Property="VerticalAlignment" Value="Center" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:SimpleControl">
                <Border x:Name="PART_MainBorder" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
                    <ContentControl x:Name="PART_body"
                                    HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                                    VerticalAlignment="{TemplateBinding VerticalAlignment}" >
                    </ContentControl>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Now as you can see this is a simple Style that will be applied to each control we instantiate in our application.

Creating the Sample

Now as we have already created our control lets add this control in MainWindow.

<Window x:Class="CustomControlExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:CustomControlExample"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition />
        </Grid.RowDefinitions>
        <local:SimpleControl Width="90" Height="90" >
        </local:SimpleControl>
        <local:SimpleControl Width="90" Height="90" Grid.Row="1" Color="Crimson">
            <local:SimpleControl.Template>
                <ControlTemplate>
                    <Border x:Name="PART_MainBorder">
                        <Button x:Name="PART_body" HorizontalAlignment="Center" VerticalAlignment="Center" Background="Black"></Button>
                    </Border>
                </ControlTemplate>
            </local:SimpleControl.Template>
        </local:SimpleControl>
    </Grid>
</Window>

So here basically I add two controls in two rows of a Grid. The first one taking up the Default Template it provides, and the second one creates its own template. You can specify the Template Part in your own template as shown in the declaration which interact with the control itself.

Now if you run the sample it shows two rectangular box, one with all default values and another with custom template.

Bit Further

There are also few things that deals with Design Support of Visual Studio and Expression Blend for a control. You can either build separate dlls for design time support for your control with .design.dll or within the same assembly. The design time support can add custom Adorners which can help in layout the control in Visual Designer or Expression Blend. To build a design time support for a control you need to inherit from IRegisterMetaData and build an AttributeTableMetaData, which will work for the Designer. I will talk about it later in a separate post.

Download the Sample Code

Conclusion

You should remember, creating a Custom control does require a lot of effort. So if you looking for just look and feel, you can get a workaround to this using custom template or inheriting from other existing Controls. But if you are building your own Custom behaviour or a library of control, then you should go for Custom Control. Also remember, custom control is Themable, Templatable and also supports inheritence, so you should always consider writing your control gracefully and hitting every possible aspect that your control might come across.

I hope this sample code will help you. If you want, you can read my WPF Tutorial too from here and subscribe to my blog too.

时间: 2024-10-13 23:04:37

Writing a Reusable Custom Control in WPF的相关文章

[WPF] 将普通的Library工程,改造成WPF Custom Control 的Library

1. 添加References PresentationCore PresentationFramework System.Xaml WindowsBase2. 修改AssemblyInfo.xsusing System.Windows; [assembly: System.Windows.ThemeInfo(ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located //(u

WPF中的Generic.xaml, theme以及custom control

原文:WPF中的Generic.xaml, theme以及custom control 在Visual Studio中创建自定义控件时,所有控件都将添加到/Themes/Generic.xaml. 最近做的项目用了个漂亮的开源UI库,结果项目临近结尾发现要支持.Net 3.5, 然而那个UI库却最低支持4.0.欲哭无泪,最后决定拿掉那个库,自己改改style得了.深刻的教训. 作为程序员一般都比较害怕搞界面,这下硬着头皮上,折腾折腾Blend,抄抄改改各种style,弄着弄着居然能看懂个大概了.

Developing your first FNC custom control

Friday, May 13, 2016 Some weeks ago, we released the TMS FNC UI Pack, a set of Framework Neutral Components (FNC), i.e. UI controls that can be used from VCL Windows applications, FireMonkey (FMX) Windows, Mac OS-X, iOS, Android applications and LCL

lightswitch binding custom control

Listing 1: Setting up data binding for the Rating control ? 1 2 3 4 5 6 usingSystem.Windows.Controls;<br>usingSystem.Windows.Data;<br>usingMicrosoft.LightSwitch.Threading;<br>namespace LightSwitchApplication <br>{<br> public

WinForm中Component Class、User Control及Custom Control的区别和使用建议

reference: http://blog.csdn.net/redstonehe/article/details/1536549 .NET Framework 为您提供了开发和实现新控件的能力.除了常见的用户控件外,现在您会发现,您可以编写能执行自身绘图的自定义控件,甚至还可以通过继承扩展现有控件的功能.确定创建何种类型的控件可能会费一番功夫.本节重点介绍了各类可继承控件间的区别,同时提供关系到为项目选择的类型的注意事项. 1. Component Class Component Class

WinForm中Component Class、User Control及Custom Control的区别和使用-转

转http://www.cnblogs.com/jhtchina/archive/2010/11/28/1028591.html NET Framework 为您提供了开发和实现新控件的能力.除了常见的用户控件外,现在您会发现,您可以编写能执行自身绘图的自定义控件,甚至还可以通过继承扩展现有控件的功 能.确定创建何种类型的控件可能会费一番功夫.本节重点介绍了各类可继承控件间的区别,同时提供关系到为项目选择的类型的注意事项. 1. Component Class Component Class可以

WinForm中Component Class、User Control及Custom Control的区别和使用

NET Framework 为您提供了开发和实现新控件的能力.除了常见的用户控件外,现在您会发现,您可以编写能执行自身绘图的自定义控件,甚至还可以通过继承扩展现有控件的功 能.确定创建何种类型的控件可能会费一番功夫.本节重点介绍了各类可继承控件间的区别,同时提供关系到为项目选择的类型的注意事项. 1. Component Class Component Class可以从任何现有的 Windows 窗体控件继承产生新的控件.此方法使您得以保留 Windows 窗体控件所有固有功能,然后通过添加自定

VS中Component Class、User Control及Custom Control的区别 .

.NET Framework 为您提供了开发和实现新控件的能力.除了常见的用户控件外,现在您会发现,您可以编写能执行自身绘图的自定义控件,甚至还可以通过继承扩展现有控件的功能.确定创建何种类型的控件可能会费一番功夫.本节重点介绍了各类可继承控件间的区别,同时提供关系到为项目选择的类型的注意事项. 1. Component Class Component Class可以从任何现有的 Windows 窗体控件继承产生新的控件.此方法使您得以保留 Windows 窗体控件所有固有功能,然后通过添加自定

Reusable async validation for WPF with Prism 5

WPF has supported validation since the first release in .NET 3.0. That support is built into the binding object and allows you to indicate validation errors through exceptions, an implementation of the IDataErrorInfo interface, or by using WPF Valida