是否以正确的方式遍历 VisualTree 从模板加载控件?

Is loading the controls from template by traversing through the VisualTree a right way?

我有一个定义了模板的自定义控件,该模板包含以下代码:

<FlipView Grid.Row="3"
          Grid.ColumnSpan="2" x:Name="FlipView1" BorderBrush="Black"
          ItemsSource="{Binding ItemsCollection, RelativeSource={RelativeSource TemplatedParent}}">
            <FlipView.ItemTemplate>
                    <DataTemplate>
                          <ScrollViewer>
                                <Grid>
                                    <local:UserControlA x:Name="PART_UserControlA"/>
                                    <Grid>
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="100" />
                                            <ColumnDefinition />
                                        </Grid.ColumnDefinitions>
                                        <local:UserControlB Grid.Column="1"
                                                            View="{Binding View}"
                                                            x:Name="PART_UserControlB"
                                                            ItemsSource="{Binding ItemsSourcePropertyOfAnItemInItemsCollection}"
                                                            ItemTemplate="{Binding TemplatePropertyOfAnItemInItemsCollection}" />
                                    </Grid>
                                </Grid>
                          </ScrollViewer>

                    </DataTemplate>
            </FlipView.ItemTemplate>
</FlipView>

在我的自定义控件后面的代码中,我有这段代码来加载模板中的控件(我不得不这样做,因为 GetTemplateChild returns null 因为 PART_UserControlB 又是FlipView 和 GetTemplateChild 的模板不会递归地获取模板化 child):

protected override void OnApplyTemplate()
{
    FlipView flipView = GetTemplateChild("FlipView1") as FlipView;
            DataTemplate dt = flipView.ItemTemplate;
            DependencyObject dio1 = dt.LoadContent();
            DependencyObject dio = (dio1 as ScrollViewer).Content as DependencyObject;

foreach (var item in FindVisualChildren<UserControlB>(dio))
            {
                if (item.Name == "PART_UserControlB")
                {
                    UserControlB controlB = item;
                    controlB.ApplyTemplate();
                    controlB.PointerPressed += OnPointerPressed;
                }
            }
}

public IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
        {
            if (depObj != null)
            {
                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
                {
                    DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                    if (child != null && child is T)
                    {
                        yield return (T)child;
                    }

                    foreach (T childOfChild in FindVisualChildren<T>(child))
                    {
                        yield return childOfChild;
                    }
                }
            }
        }

问题是当我点击 UserControlB 中的项目时,它不会触发该控件的 OnPointerPressed 事件。就像我在后面的代码中没有得到相同的 UserControlB 实例。

当您检索模板 Child(如您的部分)时,您应该使用 FrameworkElement.GetTemplateChild

检索它

你的情况:

UserControlB controlB = GetTemplateChild("PART_UserControlB") as UserControlB;

所以回答标题中的问题:不,这不是正确的做法。

此外,我认为您不应该在此处对其调用 ApplyTemplate()。

我在这里看到的另一件事是模板中没有 ElementName 或 RelativeSource 的绑定:这是一件非常糟糕的事情。您无法保证您的自定义 Control DataContext 在运行时将是什么。这将导致意外行为。

模板中的所有绑定都应将模板 parent 或模板内的可视控件作为目标,但不应使用 DataContext。

编辑

好的,所以我再次阅读了您的代码,您的 PART_UserControlB 在 DataTemplate 中,在 ItemsControl 的 ItemTemplate 中,这意味着对于 ItemsControl 中的每个项目,您将有一个名为 PART_UserControlB 的 UserControlB。您注意到的行为是正常的:您找到第一个名为 PART_UserControlB 的控件,并将事件处理程序放在它的一个事件上。但是所有其他 UserControlB 呢?

您在这里并没有真正使用模板 child,您指的是根据 ItemsControl 内容可能存在或不存在的事物。这些不是自定义控件的一部分,因此不应命名为 PART_xxx。您可以使用的是 UserControlB 中的命令 DP,它将在引发事件时执行:

//in your UserControlB.cs
public event EventHandler<YourEventArgs> PointerPressed;
private void OnPointerPressed() {
    YourEventArgs arg = new YourEventArgs();
    if (PointerPressed != null) {
        PointerPressed(this, arg);
    }
    if (PointerPressedCommand != null &&    PointerPressedCommand.CanExecute(PointerPressedCommandParameter)) {
        PointerPressedCommand.Execute(PointerPressedCommandParameter);
    }
}

#region PointerPressedCommand 
public ICommand PointerPressedCommand
{
    get { return (ICommand)GetValue(PointerPressedCommandProperty); }
    set { SetValue(PointerPressedCommandProperty, value); }
}

private readonly static FrameworkPropertyMetadata PointerPressedCommandMetadata = new FrameworkPropertyMetadata {
    DefaultValue = null,
    DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};

public static readonly DependencyProperty PointerPressedCommandProperty = 
    DependencyProperty.Register("PointerPressedCommand", typeof(ICommand), typeof(UserControlB), PointerPressedCommandMetadata);
#endregion

然后将命令绑定到您的 TemplatedParent 中的命令。

//in your Template
<local:UserControlB Grid.Column="1"
    View="{Binding View}"
    x:Name="PART_UserControlB"
    ItemsSource="{Binding ItemsSourcePropertyOfAnItemInItemsCollection}"
    ItemTemplate="{Binding TemplatePropertyOfAnItemInItemsCollection}" 
    PointerPressedCommand="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=MyCommand}"/>

使用事件处理程序可能是一个选项,但这将是一场噩梦:您将不得不观察 ItemsControl 的 ItemsSource 的变化,通过可视化树并添加处理程序。这将是一种快速但有点肮脏的方式来实现你想要实现的目标。