Dom Sinclair
Dom Sinclair

Reputation: 2528

How do I ensure that onApplyTemplate gets called before anything else

I have a wpf Custom Control on which I have been working. It has a shared New like this:

Shared Sub New()
    'This OverrideMetadata call tells the system that this element wants to provide a style that is different than its base class.
    'This style is defined in themes\generic.xaml
    DefaultStyleKeyProperty.OverrideMetadata(GetType(VtlDataNavigator_24), New FrameworkPropertyMetadata(GetType(VtlDataNavigator_24)))
    ItemsSourceProperty.OverrideMetadata(GetType(VtlDataNavigator_24), New FrameworkPropertyMetadata(Nothing, AddressOf OnItemsSourceHasChanged))
End Sub

If an Items source has been set for the custom control this shared sub then invokes the overrideMetadata for the itemssource (as shown below)

  Private Shared Sub OnItemsSourceHasChanged(ByVal d As DependencyObject, ByVal baseValue As Object)
    Dim vdn As VtlDataNavigator_24 = DirectCast(d, VtlDataNavigator_24)
    vdn.RecordCount = vdn.Items.SourceCollection.Cast(Of Object)().Count()
    vdn.MyBaseCollection = DirectCast(vdn.ItemsSource, ICollectionView)
    vdn.MyBaseEditableCollection = DirectCast(vdn.ItemsSource, IEditableCollectionView)
    vdn.MyBaseCollection.MoveCurrentToFirst
    vdn.RecordIndex = vdn.MyBaseCollection.CurrentPosition + 1
    If Not IsNothing(vdn.FindButton) Then
        If vdn.FindButton.Visibility = Visibility.Visible Then
            vdn.RecordIndexTextBox.IsReadOnly = False
        Else
            vdn.RecordIndexTextBox.IsReadOnly = True
        End If
    End If
    vdn.ResetTheNavigationButtons
    vdn.SetupInitialStatesForNonNavigationButtons
End Sub

This then fails because buttons referred to in the code (and routines called from it) have not yet been instantiated because the override for OnApplyTemplate (shown below) has not been called.

Public Overrides Sub OnApplyTemplate()
    MyBase.OnApplyTemplate()
    RecordIndexTextBox = CType(GetTemplateChild("PART_RecordIndexTextBox"), TextBox)
    RecordCountTextBox = CType(GetTemplateChild(RecordCountTextBoxPart), TextBox)
    RecordTextBlock = CType(GetTemplateChild(RecordTextBlockPart), TextBlock)
    OfTextBlock = CType(GetTemplateChild(OfTextBlockPart), TextBlock)
    FirstButton = CType(GetTemplateChild(FirstButtonPart), Button)
    PreviousButton = CType(GetTemplateChild(PreviousButtonPart), RepeatButton)
    NextButton = CType(GetTemplateChild(NextButtonPart), RepeatButton)
    LastButton = CType(GetTemplateChild(LastButtonPart), Button)
    AddButton = CType(GetTemplateChild(AddButtonPart), Button)
    CancelNewRecordButton = CType(GetTemplateChild(CancelNewButtonPart), Button)
    EditButton = CType(GetTemplateChild(EditButtonPart), button)
    CancelButton = CType(GetTemplateChild(CancelButtonPart), Button)
    RefreshButton = CType(GetTemplateChild(RefreshButtonPart), Button)
    SaveButton = CType(GetTemplateChild(SaveButtonPart), Button)
    DeleteButton = CType(GetTemplateChild(DeleteButtonPart), Button)
    FindButton = CType(GetTemplateChild(FindButtonPart), Button)
End Sub

If I add something along the lines of:

vdn.OnApplyTemplate

to OnItemsSourceHasChanged, OnApplyTemplate is called but nothing is resolved (see illustration below).

enter image description here

BUT if I don't set an itemssource on my control, then OnApplyTemplate gets called and the items resolve (see below)

enter image description here

Has anyone encountered this sort of behaviour before and found a way to correct it such that OnApplyTemplate is always the first thing to get called before anything that might require access to controls that have yet to be resolved.

Edit

The curious thing about this issue is that (and doesn't this always seem to be the case!) this was working until obviously I did something or set some property. What I am left with is a project that runs if I do not set an Items source on my custom control, and one which doesn't if I do because the custom handler I have in place to handle when the items source is changed on my custom control is running before OnApplyTemplate gets called.

Well I have at last been able to determine that my custom controls Itemssource property is being changed before the control is being drawn and rendered and therefore the code I have in place to set things up following the ItemsSource change raises null reference exceptions because the main control has yet to be rendered.

Given that it did work it must be something I've done but I'm now out od ideas as to how to delve into this further and actually find the reason. I'd welcome any suggestions you might have or potential work rounds.

Edit in relation to comments below: typical part of control template.

<!-- First Button -->

                        <Button Style="{StaticResource vtlNavButtonStyle}"
                                x:Name="PART_FirstButton"
                                Tag="First_Button"
                                Visibility="{Binding Path=NavigationButtonVisibility,Converter={StaticResource booltovis}, RelativeSource={RelativeSource TemplatedParent}}"
                                ToolTipService.ShowOnDisabled="False"
                                ToolTipService.ShowDuration="3000"
                                ToolTipService.InitialShowDelay="500">
                            <Button.ToolTip>
                                <Binding Path="FirstButtonToolTip"
                                         RelativeSource="{RelativeSource TemplatedParent}"
                                         TargetNullValue="{x:Static p:Resources.FirstText}">
                                </Binding>
                            </Button.ToolTip>
                            <StackPanel>
                                <Image Style="{StaticResource vtlImageStyle}">
                                    <Image.Source>
                                        <Binding Path="FirstImage"
                                                 RelativeSource="{RelativeSource TemplatedParent}">
                                            <Binding.TargetNullValue>
                                                <ImageSource>/VtlWpfControls;component/Images/16/first.png</ImageSource>
                                            </Binding.TargetNullValue>
                                        </Binding>
                                    </Image.Source>
                                </Image>
                            </StackPanel>
                        </Button>

Upvotes: 1

Views: 4650

Answers (3)

StayOnTarget
StayOnTarget

Reputation: 13047

I had a similar issue - a custom control (specifically, a class derived from Control) would show binding errors whenever a new instance of the control was instantiated. This was because the control template was being created before the bindings were setup. Once the bindings took effect, then the control would start to work.

To "fix" this (or work around it anyway) I just added a call to ApplyTemplate() to the control's constructor. So it ends up looking like this:

public CustomControl()
{
        InitializeComponent();
        ApplyTemplate();
}

Then there were no more binding errors.

Upvotes: 0

Steven Rands
Steven Rands

Reputation: 5421

This isn't an answer to your question, but instead expands on some things you mentioned in the comments.

I really think that it would benefit you to look into WPF commands as they pertain to custom controls. Your data navigator control sounds like it essentially supports a number of actions (go to first/previous/next/last; add; edit; cancel; etc) that you invoke using Button controls in the control template. Rather than looking for the buttons in OnApplyTemplate (at which point you store references to them so that you can presumably hook into their Click event later) you should support commands in your control: the buttons in the template would then bind to these commands.

An example would probably make this a bit clearer. The following is code for a custom control that supports two actions: go-to-first-page, and go-to-last-page. In the static constructor I register two command bindings, one for each action. These work by calling into a helper method that takes the command to "bind" to, plus a pair of delegates that get called when the action is invoked.

The commands I am using here are provided by the WPF framework, and are static properties contained in the static NavigationCommands class. (There are a bunch of other similar classes containing commands, just follow the links in the "See Also" section of that MSDN page).

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

namespace StackOverflow
{
    public class TestControl : Control
    {
        static TestControl()
        {
            RegisterCommandBinding<TestControl>(NavigationCommands.FirstPage,
                x => x.GoToFirstPage());

            RegisterCommandBinding<TestControl>(NavigationCommands.LastPage,
                x => x.GoToLastPage(), x => x.CanGoToLastPage());

            DefaultStyleKeyProperty.OverrideMetadata(typeof(TestControl),
                new FrameworkPropertyMetadata(typeof(TestControl)));
        }

        void GoToFirstPage()
        {
            Console.WriteLine("first page");
        }

        void GoToLastPage()
        {
            Console.WriteLine("last page");
        }

        bool CanGoToLastPage()
        {
            return true;  // Would put your own logic here obviously
        }

        public static void RegisterCommandBinding<TControl>(
            ICommand command, Action<TControl> execute) where TControl : class
        {
            RegisterCommandBinding<TControl>(command, execute, target => true);
        }

        public static void RegisterCommandBinding<TControl>(
            ICommand command, Action<TControl> execute, Func<TControl, bool> canExecute)
            where TControl : class
        {
            var commandBinding = new CommandBinding(command,
                (target, e) => execute((TControl) target),
                (target, e) => e.CanExecute = canExecute((TControl) target));

            CommandManager.RegisterClassCommandBinding(typeof(TControl), commandBinding);
        }
    }
}

The following is the control's default template. As you can see there are simply two Button controls, each one of which binds to the relevant command via its Command property (note this is not a data binding, ie. you're not using the {Binding} markup extension).

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:StackOverflow">
    <Style TargetType="{x:Type local:TestControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:TestControl}">
                    <StackPanel Orientation="Horizontal">
                        <Button Command="NavigationCommands.FirstPage" Content="First" />
                        <Button Command="NavigationCommands.LastPage" Content="Last" />
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Finally, here's the custom control in a Window. As you click the "First" and "Last" buttons you can see the actions being invoked by watching the relevant text appear in the debug console window.

<Window x:Class="StackOverflow.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:StackOverflow">
    <local:TestControl VerticalAlignment="Top" />
</Window>

If you use commands in this way then you should be able to simplify your control's code significantly.

Upvotes: 1

Petter Hesselberg
Petter Hesselberg

Reputation: 5518

Calling OnApplyTemplate yourself isn't going to help; the framework will call it when the template has actually been applied. That said, the order in which things happen is not deterministic -- the template may or may not be applied before the ItemsSource is set. I'm working with UWP apps for Windows 10, which is a slightly different beast, but we've solved a similar issue doing something like this:

private TextBlock textBlock;

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

    // Grab the template controls, e.g.:
    textBlock = GetTemplateChild("MyTextBlock") as TextBlock;

    InitializeDataContext();
    DataContextChanged += (sender, args) => InitializeDataContext();
}

private void InitializeDataContext()
{
    ViewModel ViewModel = DataContext as ViewModel;
    if (viewModel != null)
    {
        // Here we know that both conditions are satisfied
        textBlock.Text = ViewModel.Name;
    }
}

The key is to not start listening for DataContextChanged until the template has been applied. If the data context has already been set, the first call to initializeDataContext takes care of things; if not, the callback takes care of things.

(In your case, replace our data context listening with items source listening, I suppose.)

Upvotes: 2

Related Questions