问题
I have a custom control which has it's template defined and the template contains below code:
<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>
In code behind of my custom control, I have this code to load the controls in the template (I had to do this trick since GetTemplateChild returns null because PART_UserControlB is again a part of the template of FlipView and GetTemplateChild does not recursively gets the templated 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;
}
}
}
}
Problem is that when I tap on an item in UserControlB, it does not trigger the OnPointerPressed event for that control. It is like I am not getting the same instance of the UserControlB in the code behind.
回答1:
When you retrieve a Template Child (like your part) you should retrieve it using FrameworkElement.GetTemplateChild
In your case:
UserControlB controlB = GetTemplateChild("PART_UserControlB") as UserControlB;
So to answer the question in the title: No it's not the right way to do it.
Furthermore I don't think you should call ApplyTemplate() on it here.
Another thing I see here is bindings without ElementName or RelativeSource in your Template: This is a REALLY bad thing. You can't guarantee what your custom Control DataContext will be at runtime. This WILL lead to unexpected behavior.
All bindings in your Template should either have the Template parent or a visual Control inside the Template as target but shouldn't ever use the DataContext.
Edit
Ok so I read your code again and your PART_UserControlB is inside a DataTemplate, inside ItemsControl's ItemTemplate, which means that for EACH item in your ItemsControl you'll have a UserControlB named PART_UserControlB. The behavior you noticed is normal: You're finding the first Control named PART_UserControlB and put an event handler on one of its event. But what about all other UserControlB?
You're not really using Template child here, you're referring to things that may or may not exist according to an ItemsControl content. Those aren't a part of your custom control and should therefore not be named PART_xxx. What you could use is a Command DP in your UserControlB that will be executed when the event is raised:
//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
And then bind the Command to a Command in your 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}"/>
Using event handler could be an option but this will be a nightmare: You'll have to watch your ItemsControl's ItemsSource for changes, get through the Visual Tree and add handlers. This would be a quick and a bit dirty way to do what you want to achieve.
来源:https://stackoverflow.com/questions/34505815/is-loading-the-controls-from-template-by-traversing-through-the-visualtree-a-rig