1

Using UWP, I have a ListView where each item contains, among other things, a Button. Tapping the button opens a MenuFlyout. Tapping one of the flyout options triggers a Click event.

In the event handler, how do I find the parent Button or, alternatively, the parent ListView item ?

Here's an extract of my XAML. Specifically, I want to find the "Note" element while in the the OnItemEdit handler.

<Page.Resources>
  <ResourceDictionary>

    <DataTemplate x:Key="NoteItemTemplate">
      <Grid>
        <Grid.ColumnDefinitions> ... </Grid.ColumnDefinitions>
        <TextBox Name="Note" />
        <Button>
          <Image Source="..." />
          <Button.Flyout>
            <MenuFlyout>
              <MenuFlyoutItem Text="Edit" Click="OnItemEdit" />
              <MenuFlyoutItem Text="Delete" Click="OnItemDelete" />
            </MenuFlyout>
          </Button.Flyout>
        </Button>

      </Grid>
    </DataTemplate>
    ...
    <local:DetailItemSelector x:Key="DetailItemSelector"
      NoteItemTemplate="{StaticResource NoteItemTemplate}"
      ...
    />

  </ResourceDictionary>
</Page.Resources>

<ListView
  x:Name = "DetailList"
  ItemsSource = "{x:Bind DetailListItems}"
  ItemTemplateSelector = "{StaticResource DetailItemSelector}">
</ListView>

The handler is defined as:

async void OnItemEdit(object sender, RoutedEventArgs e)
{
  ...
}

EDIT - work around

If there's no better way, here's a work around that is not too bad. The trick is to use FlyoutBase.AttachedFlyout instead of Button.Flyout.

    <DataTemplate x:Key="NoteItemTemplate">
      <Grid>
        <Grid.ColumnDefinitions> ... </Grid.ColumnDefinitions>

        <TextBox ... Name="Note" />

        <Button ... Click=="onMoreClicked">
          <Image Source="..." />
          FlyoutBase.AttachedFlyout
            <MenuFlyout>
              <MenuFlyoutItem Text="Edit" Click="OnItemEdit" />
              <MenuFlyoutItem ... />
            </MenuFlyout>
          </FlyoutBase.AttachedFlyout>
        </Button>

      </Grid>
    </DataTemplate>

And, code behind

FrameworkElement lastItemTapped = null;

public void onMoreClicked (object sender, RoutedEventArgs e)
{
  Button button = sender as Button;

  // find parent list item containing button
  DependencyObject element = button;
  while (element != null)
  {
    if (element is FrameworkElement && (element as FrameworkElement).Name.Equals("Item"))
      break;
    element = VisualTreeHelper.GetParent(element);
  }
  if (element != null)
    lastItemTapped = element as FrameworkElement;

  // show flyout menu
  FlyoutBase.ShowAttachedFlyout(button);
}

public void OnItemEdit(object sender, RoutedEventArgs e)
{
  if (lastItemTapped == null)
    return;
  ...
}
Peri Hartman
  • 19,314
  • 18
  • 55
  • 101

2 Answers2

1

Specifically, I want to find the "Note" element while in the the OnItemEdit handler.

Assuming the DataTemplate is applied to a "Note", that should be as easy as casting the DataContext of the sender argument to a Note (or whatever your type is called):

private void OnItemEdit(object sender, RoutedEventArgs e)
{
    MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender;
    var note = menuFlyoutItem.DataContext as YourNoteClass;
    ...
}

If you need a reference to the "Node" TextBox element, you can find it in the visual tree:

private void OnItemEdit(object sender, RoutedEventArgs e)
{
    MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender;
    DependencyObject container = DetailList.ContainerFromItem(menuFlyoutItem.DataContext);
    TextBox note = FindVisualChild<TextBox>(container);
    ...
}

private static T FindVisualChild<T>(DependencyObject visual) where T : DependencyObject
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(visual, i);
        if (child != null)
        {
            T correctlyTyped = child as T;
            if (correctlyTyped != null)
                return correctlyTyped;

            T descendent = FindVisualChild<T>(child);
            if (descendent != null)
                return descendent;
        }
    }
    return null;
}
mm8
  • 163,881
  • 10
  • 57
  • 88
  • Sorry, not yet. I most certainly will. Probably thursday. – Peri Hartman Feb 10 '21 at 05:17
  • If I do what you suggest, `DataContext` points to the note data (a string of text), not the note TextBox. I've been studying this and experimenting a bit, but can't figure out how to set the `DataContext` to the note control. I tried adding `DataContext="{StaticResource Item}"` to the "Edit" `MenuFlyoutItem`, but that caused a runtime exception. Searching online turned up nothing for this kind of case. – Peri Hartman Feb 11 '21 at 17:11
  • I suppose I could use the search the `ListView` for an item containing the referenced note data. Easy enough, but I only want to do that if there isn't a more direct solution. – Peri Hartman Feb 11 '21 at 17:37
  • @PeriHartman: See my edit. It should answer your question. – mm8 Feb 11 '21 at 20:52
  • That returns null for `container`, in `OnItemEdit`. – Peri Hartman Feb 11 '21 at 21:58
  • I can't find any documentation for `ContainerFromItem`, other than the terse API description, so I'm not really sure what it's supposed to do. One thing I do suspect: the flyout is implemented on a transparent page overlaying my page. Thus the root of the flyout is completely separate from my controls. I like the idea of using a pointer to a `DataContext` if only it can be made to work. – Peri Hartman Feb 11 '21 at 22:04
  • @PeriHartman: If you need any further help, please post a complete but minimal example that can be used to reproduce your issue from scratch. `ContainerFromItem` returns the visual container for the current object in the `ListView`. – mm8 Feb 12 '21 at 10:10
  • Thanks, mm8. I may produce a complete example later, as I would like to fully understand `DataContext` and `ContainerFromItem`. But I have some other priorities right now. – Peri Hartman Feb 12 '21 at 15:08
0

In your scenario, you could add a member variable to save the Grid instance which represents a ListViewItem for later access. Add a PointerEntered event to the Grid panel. When you click an item’s button to pop up a flyout, the PointerEntered event handler of the item’s Grid will be triggered first. In PointerEntered event handler, assign the sender to the member variable. Then you could access the Grid in OnItemEdit event handler.

You could check the following as a sample:

//MainPage.xaml
<DataTemplate x:Key="NoteItemTemplate">
  <Grid PointerEntered="Grid_PointerEntered" >
    <Grid.ColumnDefinitions> ... </Grid.ColumnDefinitions>
    ……
  </Grid>
</DataTemplate>

//MainPage.xaml.cs

private Grid myPanel;

private void Grid_PointerEntered(object sender, PointerRoutedEventArgs e)
{
    myPanel = sender as Grid;
}

private void OnItemEdit(object sender, RoutedEventArgs e)
{
    //Change the value of index to adjust your scenario
    var box = VisualTreeHelper.GetChild(myPanel, 0) as TextBox;
}

Update:

Please try the following code:

private void OnItemEdit(object sender, RoutedEventArgs e)
{            
    //Find the item be clicked
    object clickedItem=new object();
    foreach(var cur in DetailList.Items)
    {
        if(cur.ToString()==(sender as MenuFlyoutItem).DataContext.ToString())
        {
            clickedItem = cur;
            break;
        }
    }
    //
    var type = DetailList.ContainerFromItem(clickedItem);
    var item = type as ListViewItem;
    var box = VisualTreeHelper.GetChild(item.ContentTemplateRoot as DependencyObject, 0);
    string text = (box as TextBox).Text;
}
YanGu
  • 3,006
  • 1
  • 3
  • 7
  • I must be misunderstanding how your explanation works. If the list has 10 items, how do I determine which item (and in turn, which TextBox) triggered the OnItemEdit event ? It appears you are arbitrarily picking one (probably the first one). – Peri Hartman Feb 05 '21 at 16:50
  • It’s not arbitrary. Before you click on a certain Button to popup a Flyout, you must move your pointer over the ListViewItem which contains the Flyout. At this time, the PointerEntered event handler will be called to assign the ListViewItem’s Grid to member variable, and no other ListViewItem’s PointerEntered event handler will be called before the Flyout hide. When you click a ListViewItem’s Button, this could ensure that the Grid stored in member variable is always the one whose Button is clicked. – YanGu Feb 08 '21 at 01:56
  • Ah, sorry, I apparently didn't understand what 'PointerEntered' does. Unfortunately I don't think this approach will work. I didn't mention (because I figured there would be a straight forward solution) that it needs to work with touch screens and I don't think 'PointerEntered' will do anything (I will test this and see). The right solution would be to handle the 'Button' click event, cache a ref to the button (or parent item) and then forward the click event to the original handler. I can't find a way to do that. Also, the 'PointerEntered' approach is somewhat fragile. – Peri Hartman Feb 08 '21 at 17:41
  • I have updated the answer, you could try to see if the update could meet your requirements. – YanGu Feb 10 '21 at 01:49
  • I already have a work-around, which I think is better. I'll add it to the OP for reference. – Peri Hartman Feb 11 '21 at 18:10