是否以正确的方式遍历 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 的变化,通过可视化树并添加处理程序。这将是一种快速但有点肮脏的方式来实现你想要实现的目标。
我有一个定义了模板的自定义控件,该模板包含以下代码:
<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 的变化,通过可视化树并添加处理程序。这将是一种快速但有点肮脏的方式来实现你想要实现的目标。