0

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.

tavier
  • 1,744
  • 4
  • 24
  • 53

1 Answers1

2

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.

nkoniishvt
  • 2,442
  • 1
  • 14
  • 29
  • GetTemplateChild returns null since PART_UserControlB is again a part of the template of FlipVIew (which is already a part of the template of my custom control). So I ended up using this trick (looping through the VisualTree). I have edited my post to mention this point. – tavier Dec 29 '15 at 08:32
  • since it's a flip view it wouldn't be containing more than 20 items in my case. So I kind of liked the quick and dirty (:)) way you mentioned. Also, in my code, I know where the items source is getting changed. So it would be easier to subscribe the event for all items in the item source. But when I try it, I am not getting the item container of the items. I suspect that as soon as I set the items source of the flip view to some collection, the view is still not loaded. Looks like I am going to have to wait until the items are loaded in the view. Thanks a lot. – tavier Dec 30 '15 at 09:33
  • @AshishAgrawal The ItemsSource contains data you transmit to the ItemsControl, not visual elements. If you want to get the visual object corresponding to a value in ItemsSource you can use: YourItemsControl.ItemContainerGenerator.ContainerFromItem(YourValue); Note that this won't work for specific types of ItemsControl (when a Virtualization Panel is used) – nkoniishvt Dec 30 '15 at 09:41
  • I am trying to get the container using myFlipView.ContainerFromIndex(i) (since ItemContainerGenerator.ContainerFromIndex(i) is deprecated after 8.1) but it returns null. I think by the time I am executing this piece of code, the UI is not loaded. Is there any trick to work that around? – tavier Dec 30 '15 at 10:04
  • Here is my code (sorry for the messed up formatting): ItemsCollection = new Collection() { new ItemTest() { ItemsSource = someCollection}, new ItemTest() { ItemsSource = someCollection}, new ItemTest() { ItemsSource = someCollection} }; FlipView flipView = GetTemplateChild("FlipVIew1) as FlipView; var container = flipView.ContainerFromIndex(0); //returns null – tavier Dec 30 '15 at 10:05
  • Try to set VirtualizingStackPanel.IsVirtualizing="False" to your FlipView. It should create all containers for its ItemsSource values instead of loading them only when needed. In the ApplyTemplate function the visual tree should already be built so it doesn't comes for here – nkoniishvt Dec 30 '15 at 10:11
  • Actually IsVirtualizing is no more settable and all I can see is VirtualizingStackPanel.VirtualizationMode attached property and I already tried setting it too but still no luck. I noticed that when I tried to move the code to get the container to a later stage (just for testing) it was loading the container fine. So to me, it looks like a timing issue. – tavier Dec 30 '15 at 10:17
  • So I gave up on the trick to subscribe to the events and started implementing the custom PointerPressedCommand as you mentioned. I have couple of questions there. 1. What is OnPointerPressed, is the overriden method? protected override void OnPointerPressed? 2. In the xaml, what would be MyCommand, since the templated parent of UserControlB would be the FlipView, where should I define MyCommand? – tavier Dec 30 '15 at 11:20
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/99303/discussion-between-nkoniishvt-and-ashish-agrawal). – nkoniishvt Dec 30 '15 at 12:16