Microsoft-ui-xaml: Proposal: Improve context menu support for listviews

Created on 21 Jun 2019  路  4Comments  路  Source: microsoft/microsoft-ui-xaml

Proposal: Improve context menu support for listviews

Summary

Adding context menus to list view items is more complicated than it should be.
Depending on where we define the menu, it can be complicated to get a contextual menu working with command binding, accelerators, and all the available input types.

Rationale

My goal was to add the same contextual menu to all my list view's items.
Today, we have several options to do that:

Define a ContextFlyout in our items' DataTemplate

<DataTemplate x:Key="itemTemplateWithLocalContextMenu">
  <Grid Background="Transparent">
    [...]
    <Grid.ContextFlyout>
      <MenuFlyout>
        <MenuFlyoutItem Command="{Binding Command}" CommandParameter="{Binding}" DataContext="{Binding}" Text="Click me from item template">
          <MenuFlyoutItem.KeyboardAccelerators>
            <KeyboardAccelerator Key="A" />
          </MenuFlyoutItem.KeyboardAccelerators>
        </MenuFlyoutItem>
      </MenuFlyout>
    </Grid.ContextFlyout>
  </Grid>
</DataTemplate>

The binding is working without any effort since we are within the data template and have access to the item
But:

  • The menu is opening only when right clicking on the DataTemplate content which is not covering completely the ListViewItem
  • Keyboard accelerators are not working
  • The keyboard context menu key is not working

Define a ContextFlyout on a custom ListViewItem style

<ListView.ItemContainerStyle>
    <Style TargetType="ListViewItem">
        <Setter Property="ContextFlyout">
            <Setter.Value>
                <MenuFlyout>
                    <MenuFlyoutItem Command="{Binding Command}" CommandParameter="{Binding}" DataContext="{Binding}" Text="Click me from ListViewItem">
                        <MenuFlyoutItem.KeyboardAccelerators>
                            <KeyboardAccelerator Key="B" />
                        </MenuFlyoutItem.KeyboardAccelerators>
                    </MenuFlyoutItem>
                </MenuFlyout>
            </Setter.Value>
        </Setter>
    </Style>
</ListView.ItemContainerStyle>

With this approach, the ContextFlyout is now opening fine when right clicking anywhere on the ListViewItem. The keyboard context menu key is also working.
But:

  • The binding is not working. The DataContext/Content of the ListViewItem is not propagated to the flyout. It requires some custom code-behind to make it works:
private void OnMenuFlyoutOpening(object sender, object e)
{
    var dataContext = Target?.DataContext ?? (Target as ContentControl)?.Content;
    foreach (var item in Items)
    {
        item.DataContext = dataContext;
    }
}
  • The keyboard accelerators are not working before opening the flyout menu.
  • We need custom code to detect the current active/selected item when the keyboard accelerator is invoked.

Define a ContextFlyout at the ListView level

<ListView
    ItemTemplate="{StaticResource itemTemplate}"
    ItemsSource="{x:Bind Items}">
    <ListView.ContextFlyout>
        <MenuFlyout>
            <MenuFlyoutItem
                Command="{x:Bind ((local:DataItem)listViewWithContextFlyout.SelectedItem).Command, Mode=OneWay}"
                CommandParameter="{x:Bind listViewWithContextFlyout.SelectedItem, Mode=OneWay}"
                Text="Click me from ListView">
                <MenuFlyoutItem.KeyboardAccelerators>
                    <KeyboardAccelerator Key="C" />
                </MenuFlyoutItem.KeyboardAccelerators>
            </MenuFlyoutItem>
        </MenuFlyout>
    </ListView.ContextFlyout>
</ListView>

This is working fine more most of the scenario but:

  • The right click is not selecting the list view items so we need to add some custom code to do it.
  • The context flyout is also displayed when clicking outside of the items. We can override the flyout opening event to force it to hide but this is a hack.

Define a ContextFlyout on a custom ListViewItem style and use XAMLUICommand

This option allow us to move the command registration outside of the ListViewItems but it still required some code to get the current/selected item from the ListView. It does not solve the missing ListViewItem.DataContext/Content inside the context flyout.
XAMLUICommand is only available starting with RS5 which make it not usable when targeting a lower Windows build.

Write custom code

Using some custom code, we can get a fully working ContextFlyout but it requires to force the list view item selection when right clicking on it, do some code to set the DataContext on the ContextFlyout content to make the binding work.

Scope

| Capability | Priority |
| :---------- | :------- |
| Defining a per item contextual menu should be straightforward | Must|
| Binding should work in a ContextFlyout | Must |
| Keyboard accelerator should "just" work executing the command on the current item | Must |

Important Notes

The definition of a per item context flyout should be easy and MVVM compliant without any need for custom code to have everything working (mouse, keyboard, accelerators).

Sample application highlighting the different behaviors and attempts.

ContextMenu.zip
ContextMenu2.zip

area-Commanding area-Lists feature proposal team-Controls

Most helpful comment

I guess a lot of devs know this problem and have coded their own workarounds. A solution in WinUI would be very appreciated.

My thoughts on this:

When the ListView is in multi selection mode, and you right-click on one of the selected items, you'd probably want the menu command to apply to all selected items. This is what Windows does everywhere.

So I'd recommend to define this as a new property on the ListView itself, e.g. ListView.ItemsMenuFlyout. The CommandParameter would always be a list of items. When right-clicking on a selected item, the list would contain all selected items. When right-clicking on an unselected item, the list would only contain the clicked item.

Note: This requires the Command to be defined on the top-level ViewModel, not on the individual items.

All 4 comments

I guess a lot of devs know this problem and have coded their own workarounds. A solution in WinUI would be very appreciated.

My thoughts on this:

When the ListView is in multi selection mode, and you right-click on one of the selected items, you'd probably want the menu command to apply to all selected items. This is what Windows does everywhere.

So I'd recommend to define this as a new property on the ListView itself, e.g. ListView.ItemsMenuFlyout. The CommandParameter would always be a list of items. When right-clicking on a selected item, the list would contain all selected items. When right-clicking on an unselected item, the list would only contain the clicked item.

Note: This requires the Command to be defined on the top-level ViewModel, not on the individual items.

There would have to be a way for some items to either disable, add or remove items from the Context Menu Flyout on a per item basis.

Making a note to also consider #1487 when designing this feature.

Regarding Define a ContextFlyout on a custom ListViewItem style, it's also possible to get the DataModel like this:

        private SampleDataModel GetDataModelForCurrentListViewFlyout()
        {
            // Obtain the ListViewItem for which the user requested a context menu.
            var listViewItem = SharedFlyout.Target;

            // Get the data model for the ListViewItem.
            return (SampleDataModel)ItemListView.ItemFromContainer(listViewItem);
        }

From here.

Was this page helpful?
0 / 5 - 0 ratings