Custom control OnApplyTemplate called after dependency property callback

Kupido picture Kupido · Aug 6, 2012 · Viewed 13.6k times · Source

I'm developing my first WPF custom control and I'm facing some problems, here's a simplified version of the code I'm currently using:

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

namespace MyControls
{
    [TemplatePart(Name = "PART_Button", Type = typeof (Button))]
    public class MyControl : Control
    {
        public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof (object), typeof (MyControl), new PropertyMetadata(null, OnLabelPropertyChanged));

        private Button _buttonElement;

        public object Content
        {
            get { return this.GetValue(LabelProperty); }
            set { this.SetValue(ContentProperty, value); }
        }

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

        private static void OnContentPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            MyControl myControl = sender as MyControl;
            if (myControl != null && myControl._buttonElement != null)
                myControl._buttonElement.Content = e.NewValue;
        }

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

            this._buttonElement = this.Template.FindName("PART_Button", this) as Button;
        }
    }
}

This is the template for my custom control:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:MyControls">
    <Style TargetType="{x:Type local:MyControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MyControl}">
                    <Button x:Name="PART_Button" />
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Then i put it inside a Grid and try to set its Content property:

<Grid x:Name="layoutRoot">
    <controls:MyControl x:Name="myControl" />
</Grid>

Here's the code behind:

using System.Windows;

namespace MyControls
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();

            this.myControl.Content = "test";
        }
    }
}

This doesn't work, for some reason the OnContentPropertyChanged callback is called before OnApplyTemplate, so myControl._buttonElement is assigned too late and it's still null when trying to set its content. Why is this happening and how can I change this behavior?

I also need to provide full design time support but I cannot find a way to make my custom control accept some additional markup, much like the Grid control does with ColumnDefinitions:

<Grid x:Name="layoutRoot">
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
</Grid>

Any help would be greatly appreciated!

UPDATE

I found a document that explains why the OnApplyTemplate method is called after control properies are set:

http://msdn.microsoft.com/en-us/library/dd351483%28v=vs.95%29.aspx

So the question is: how can I keep track of the properties that are set (in XAML or programmatically) and methods that are called when the control has not been initialized, so that I can set/call them when the OnApplyTemplate method is called? How can the same callback/method work both before and after the control initialization without duplicating the code?

Answer

Colin Smith picture Colin Smith · Aug 6, 2012

UPDATE:

Instead of your "property" pushing changes in value into elements in your template by looking for Parts, you should instead have your template bind to properties on the control being templated.

Normally this is done using presenters within a template e.g. ContentPresenter binds to the property designated as the "content" (it finds out that name by looking for the [ContentProperty] attribute), and by using bindings in your template that use TemplateBinding or TemplatedParent to connect to the properties on your custom control.

Then there is no issue about what order you set your properties and when the template is applied....because it is the template that provides the "look" for the data/properties set on your control.

A custom control should only really need to know and interact with "parts" if it needs to provide certain behaviour/functionality e.g. hooking the click event on a button "part".

In this case instead of setting the Content in the constructor in code-behind, you should get your template to bind to the property. The example I gave below showed how that was generally done with a Content property.

Alternatively you could pull out properties more explicitly e.g. this could be inside your template.

 <Label Content="{TemplateBinding MyPropertyOnMyControl}" .....

 <Button Content="{TemplateBinding AnotherPropertyOnMyControl}" .....

I think it would be better to designate your "content" using [ContentProperty] attribute, and then using a ContentPresenter in your template so that it can be injected inside your Button, rather than you hooking your Content DependencyProperty. (if you inherit from ContentControl then that provides the "content" behaviour).

[TemplatePart(Name = "PART_Button", Type = typeof (Button))]
public class MyControl : Control
[ContentProperty("Content")]

and

<ControlTemplate TargetType="{x:Type local:MyControl}">
<Button x:Name="PART_Button">
<ContentPresenter/>
</Button>
</ControlTemplate>

As for you wanting to be able to specify some design time data via XAML like Grid does with ColumnDefinition....well that is just using Property Element syntax to specify the items to fill an IList/ICollection typed property.

So just create your own property that can hold a collection of the type you accept e.g.

public List<MyItem> MyItems { get; set; }    // create in your constructor.