我如何确保在其他任何事情之前调用onApplyTemplate

时间:2015-12-13 16:21:08

标签: c# wpf vb.net xaml visual-studio-2015

我有一个wpf自定义控件,我一直在工作。它有一个像这样的共享New:

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

如果已为自定义控件设置了一个Items源,则此共享子将调用itemssource的overrideMetadata(如下所示)

  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

然后失败,因为代码中引用的按钮(以及从它调用的例程)尚未实例化,因为尚未调用OnApplyTemplate(如下所示)的覆盖。

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

如果我添加以下内容:

vdn.OnApplyTemplate

到OnItemsSourceHasChanged,调用OnApplyTemplate但没有解决任何问题(参见下图)。

enter image description here

但如果我没有在我的控件上设置itemssource,那么OnApplyTemplate会被调用并且项目会解析(见下文)

enter image description here

之前有没有人遇到过这种行为,并找到了纠正它的方法,以便在任何可能需要访问尚未解析的控件之前,首先要调用OnApplyTemplate。

修改

关于这个问题的奇怪之处在于(并且似乎并非总是这样!)这种情况一直有效,直到显然我做了一些事情或设置了一些属性。我剩下的是一个项目,如果我没有在我的自定义控件上设置一个Items源,那就会运行一个项目,如果我这样做就不会因为我已经有了自定义处理程序在项目源被更改时处理在调用OnApplyTemplate之前,我的自定义控件正在运行。

我终于能够确定在绘制和呈现控件之前我的自定义控件Itemssource属性正在被更改,因此我在ItemsSource更改后设置的代码会引发null引用异常,因为主要控制尚未呈现。

鉴于它确实有效,它必须是我已经完成的事情,但我现在已经想出如何进一步深入研究这个问题,并找到理由。我欢迎您提出任何建议或潜在的工作轮次。

与以下评论相关的

编辑:控制模板的典型部分。

<!-- 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>

3 个答案:

答案 0 :(得分:2)

自己调用OnApplyTemplate无济于事;框架将在实际应用模板时调用它。也就是说,事情发生的顺序不确定 - 在设置ItemsSource之前可能会或可能不会应用模板。我正在使用Windows 10的UWP应用程序,这是一个略有不同的野兽,但我们已经解决了类似的问题:

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;
    }
}

关键是在应用模板之前不要开始监听DataContextChanged。如果已经设置了数据上下文,则第一次调用initializeDataContext会处理事情;如果没有,回调会处理事情。

(在你的情况下,我想用项目源监听来替换我们的数据上下文监听。)

答案 1 :(得分:1)

这不是你问题的答案,而是扩展你在评论中提到的一些事情。

我真的认为查看WPF命令会对您有所帮助,因为它们与自定义控件有关。您的数据导航器控件听起来基本上支持您使用控件模板中的按钮控件调用的许多操作(转到第一个/上一个/下一个/最后一个;添加;编辑;取消;等等)。而不是在OnApplyTemplate中查找按钮(此时您存储对它们的引用以便以后可能会将其挂钩到Click事件中),您应该支持命令控制:然后模板中的按钮将绑定到这些命令。

一个例子可能会让这一点更加清晰。以下是支持两个操作的自定义控件的代码:go-to-first-page和go-to-last-page。在静态构造函数中,我注册了两个命令绑定,每个操作一个。这些工作通过调用一个带有命令“绑定”的辅助方法,再加上调用操作时调用的一对委托。

我在这里使用的命令由WPF框架提供,并且是静态NavigationCommands类中包含的静态属性。 (还有许多其他类似的类包含命令,只需按照该MSDN页面的“另请参阅”部分中的链接)。

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);
        }
    }
}

以下是控件的默认模板。正如您所看到的,只有两个 Button 控件,每个通过其Command属性将绑定到相关命令(请注意,这不是 >数据绑定,即你没有使用{Binding}标记扩展名。)

<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>

最后,这是窗口中的自定义控件。当您单击“First”和“Last”按钮时,您可以通过观察调试控制台窗口中显示的相关文本来查看正在调用的操作。

<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>

如果以这种方式使用命令,那么您应该能够显着简化控件的代码。

答案 2 :(得分:0)

我遇到了类似的问题 - 自定义控件(特别是从 Control 派生的类)会在实例化控件的新实例时显示绑定错误。这是因为在设置绑定之前创建了控件模板。一旦绑定生效,控件就会开始工作。

为了“解决”这个问题(或者无论如何都要解决它),我只是在控件的构造函数中添加了对 ApplyTemplate() 的调用。所以它最终看起来像这样:

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

然后就没有更多的绑定错误了。