集合和列表的上下文命令

许多应用包含列表、网格和树形结构的内容集合,用户可以对这些内容进行操作。 例如,用户可能能够删除、重命名、标记或刷新项。 本文介绍如何使用上下文命令来实现这些操作,以便为所有输入类型提供最佳的用户体验。

重要 APIICommand 接口UIElement.ContextFlyout 属性INotifyPropertyChanged 接口

使用各种输入执行“收藏夹”命令

为所有输入类型创建命令

由于用户可以使用广泛的设备和输入与 Windows 应用交互,因此你的应用应通过输入无关的上下文菜单和输入特定的快捷键来提供命令。 包括这两者都允许用户快速调用内容上的命令,而不考虑输入或设备类型。

下表显示了一些典型的集合命令和公开这些命令的方法。

Command 与输入无关 鼠标加速器 键盘快捷键 触摸加速器
删除项目 上下文菜单 悬停按钮 DEL 键 轻扫以删除
标记项 上下文菜单 悬停按钮 Ctrl+Shift+G 轻扫到标志
刷新数据 上下文菜单 N/A F5 键 下拉以刷新
收藏项目 上下文菜单 悬停按钮 F、Ctrl+S 轻扫到收藏夹
  • 一般情况下,应该让项的所有命令在项的 上下文菜单中可用。 无论输入类型如何,用户都可以访问上下文菜单,并且应包含用户可执行的所有上下文命令。

  • 对于经常访问的命令,请考虑使用输入加速器。 输入加速器允许用户根据输入设备快速执行操作。 输入加速器包括:

    • 轻扫操作(触控加速器)
    • 下拉刷新数据(触控加速器)
    • 键盘快捷方式(键盘快捷键)
    • 访问键(键盘快捷键)
    • 鼠标和笔悬停按钮 (指针加速器)

注释

用户应能够从任何类型的设备访问所有命令。 例如,如果应用的命令仅通过悬停按钮指针加速器公开,触摸用户将无法访问它们。 至少使用上下文菜单提供对所有命令的访问权限。

示例:PodcastObject 数据模型

为了展示我们的推荐能力,本文为播客应用程序设计了一份播客列表。 示例代码演示如何让用户从列表中“收藏”特定播客。

以下是我们要处理的播客对象的定义:

public class PodcastObject : INotifyPropertyChanged
{
    // The title of the podcast
    public String Title { get; set; }

    // The podcast's description
    public String Description { get; set; }

    // Describes if the user has set this podcast as a favorite
    public bool IsFavorite
    {
        get
        {
            return _isFavorite;
        }
        set
        {
            _isFavorite = value;
            OnPropertyChanged("IsFavorite");
        }
    }
    private bool _isFavorite = false;

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(String property)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
    }
}

请注意,当用户切换 IsFavorite 属性时,PodcastObject 实现 INotifyPropertyChanged 以响应属性更改。

使用 ICommand 接口定义命令

ICommand 接口可帮助你定义可用于多个输入类型的命令。 例如,不需要在两个不同的事件处理程序中重复编写相同的 Delete 命令代码,一个用于用户按下 Delete 键,另一个用于用户在上下文菜单中右键单击“删除”时,您可以实现一次删除逻辑(作为 ICommand),然后将其应用于不同的输入类型。

我们需要定义表示“收藏”动作的 ICommand。 我们将使用命令的 Execute 方法收藏播客。 特定播客将通过命令的参数提供给执行方法,该参数可以使用 CommandParameter 属性进行绑定。

public class FavoriteCommand: ICommand
{
    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {
        return true;
    }
    public void Execute(object parameter)
    {
        // Perform the logic to "favorite" an item.
        (parameter as PodcastObject).IsFavorite = true;
    }
}

若要对多个集合和元素使用相同的命令,可以将命令存储为页面或应用上的资源。

<Application.Resources>
    <local:FavoriteCommand x:Key="favoriteCommand" />
</Application.Resources>

若要执行命令,请调用其 Execute 方法。

// Favorite the item using the defined command
var favoriteCommand = Application.Current.Resources["favoriteCommand"] as ICommand;
favoriteCommand.Execute(PodcastObject);

创建 UserControl 以响应各种输入

如果你有一个项列表,其中每个项都应响应多个输入,则可以通过为项定义 UserControl 并使用它来定义项目的上下文菜单和事件处理程序来简化代码。

在 Visual Studio 中创建 UserControl:

  1. 在解决方案资源管理器中,右键单击项目。 上下文菜单随即打开。
  2. 选择 “添加新 > 项...”
    此时会显示“ 添加新项 ”对话框。
  3. 从项列表中选择 UserControl。 为它指定所需名称,然后单击“ 添加”。 Visual Studio 将为你生成一个 UserControl 模板。

在我们的播客示例中,每个播客都将显示在列表中,该列表将公开各种用于“收藏”播客的方法。 用户将能够执行以下操作来“收藏”播客:

  • 调用上下文菜单
  • 执行键盘快捷方式
  • 显示悬停按钮
  • 执行轻扫手势

为了封装这些行为并使用 FavoriteCommand,我们创建一个名为“PodcastUserControl”的新 UserControl,以表示列表中的播客。

PodcastUserControl 将 PodcastObject 的字段显示为 TextBlock,并响应不同类型的用户交互。 我们将在整个本文中引用并扩展播客用户控制功能。

PodcastUserControl.xaml

<UserControl
    x:Class="ContextCommanding.PodcastUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    IsTabStop="True" UseSystemFocusVisuals="True"
    >
    <Grid Margin="12,0,12,0">
        <StackPanel>
            <TextBlock Text="{x:Bind PodcastObject.Title, Mode=OneWay}" Style="{StaticResource TitleTextBlockStyle}" />
            <TextBlock Text="{x:Bind PodcastObject.Description, Mode=OneWay}" Style="{StaticResource SubtitleTextBlockStyle}" />
            <TextBlock Text="{x:Bind PodcastObject.IsFavorite, Mode=OneWay}" Style="{StaticResource SubtitleTextBlockStyle}"/>
        </StackPanel>
    </Grid>
</UserControl>

PodcastUserControl.xaml.cs

public sealed partial class PodcastUserControl : UserControl
{
    public static readonly DependencyProperty PodcastObjectProperty =
        DependencyProperty.Register(
            "PodcastObject",
            typeof(PodcastObject),
            typeof(PodcastUserControl),
            new PropertyMetadata(null));

    public PodcastObject PodcastObject
    {
        get { return (PodcastObject)GetValue(PodcastObjectProperty); }
        set { SetValue(PodcastObjectProperty, value); }
    }

    public PodcastUserControl()
    {
        this.InitializeComponent();

        // TODO: We will add event handlers here.
    }
}

请注意,PodcastUserControl 维护对 PodcastObject 作为 DependencyProperty 的引用。 这使我们可以将 PodcastObjects 绑定到 PodcastUserControl。

生成一些 PodcastObjects 后,可以通过将 PodcastObjects 绑定到 ListView 来创建播客列表。 PodcastUserControl 对象描述 PodcastObjects 的可视化效果,因此使用 ListView 的 ItemTemplate 进行设置。

MainPage.xaml

<ListView x:Name="ListOfPodcasts"
            ItemsSource="{x:Bind podcasts}">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="local:PodcastObject">
            <local:PodcastUserControl PodcastObject="{x:Bind Mode=OneWay}" />
        </DataTemplate>
    </ListView.ItemTemplate>
    <ListView.ItemContainerStyle>
        <!-- The PodcastUserControl will entirely fill the ListView item and handle tabbing within itself. -->
        <Style TargetType="ListViewItem" BasedOn="{StaticResource ListViewItemRevealStyle}">
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            <Setter Property="Padding" Value="0"/>
            <Setter Property="IsTabStop" Value="False"/>
        </Style>
    </ListView.ItemContainerStyle>
</ListView>

创建上下文菜单

当用户请求命令或选项时,上下文菜单将显示命令或选项的列表。 上下文菜单提供与其附加元素相关的上下文命令,通常用于该项的特定次要操作。

显示项上的上下文菜单

用户可以通过以下“上下文操作”来调用上下文菜单:

Input 上下文操作
鼠标 右键单击
键盘 Shift+F10、菜单按钮
触控 长按项目
触笔 “桶”按钮按下,长按项目
游戏手柄 菜单按钮

由于用户可以打开上下文菜单而不考虑输入类型,因此上下文菜单应包含可用于列表项的所有上下文命令。

ContextFlyout

由 UIElement 类定义的 ContextFlyout 属性可以轻松创建适用于所有输入类型的上下文菜单。 你提供一个使用 MenuFlyout 或 CommandBarFlyout 作为上下文菜单的浮出控件,当用户执行上面定义的“上下文操作”时,将显示与该项对应的 MenuFlyout 或 CommandBarFlyout。

请参阅菜单和上下文菜单以帮助识别菜单与上下文菜单的场景,并指导何时使用菜单浮出控件命令栏浮出控件

在本示例中,我们将使用 MenuFlyout,首先将 ContextFlyout 添加到 PodcastUserControl。 指定为 ContextFlyout 的 MenuFlyout 包含一个用于收藏播客的选项。 请注意,此 MenuFlyoutItem 使用上面定义的 favoriteCommand,并将 CommandParameter 绑定到 PodcastObject。

PodcastUserControl.xaml

<UserControl>
    <UserControl.ContextFlyout>
        <MenuFlyout>
            <MenuFlyoutItem Text="Favorite" Command="{StaticResource favoriteCommand}" CommandParameter="{x:Bind PodcastObject, Mode=OneWay}" />
        </MenuFlyout>
    </UserControl.ContextFlyout>
    <Grid Margin="12,0,12,0">
        <!-- ... -->
    </Grid>
</UserControl>

请注意,您还可以使用 ContextRequested 事件来响应上下文操作。 如果指定了 ContextFlyout,则 ContextRequested 事件将不会触发。

创建输入加速器

尽管集合中的每个项都应有一个包含所有上下文命令的上下文菜单,但你可能希望让用户能够快速执行一组较小的常用命令。 例如,邮件应用可能具有辅助命令,如“回复”、“存档”、“移动到文件夹”、“设置标志”和“删除”,这些命令显示在上下文菜单中,但最常见的命令是“删除”和“标志”。 确定最常见的命令后,可以使用基于输入的加速器使这些命令更易于用户执行。

在播客应用中,经常使用的命令是“收藏”命令。

键盘加速器

快捷方式和直接键处理

按住 Ctrl 和 F 键以执行操作

根据内容类型,您可能会确定应执行某些操作的键组合。 例如,在电子邮件应用中,DEL 密钥可用于删除所选电子邮件。 在播客应用中,Ctrl+S 或 F 键可以收藏播客以供以后使用。 尽管某些命令具有常见的已知键盘快捷方式(如 DEL)来删除,但其他命令具有特定于应用或域的快捷方式。 如果可能,请使用已知的快捷方式,或者考虑在 工具提示 中提供提醒文本来向用户介绍快捷方式命令。

当用户使用 KeyDown 事件按下键时,应用可以做出响应。 一般情况下,用户期望应用在首次按下键时会做出响应,而不是等到他们释放密钥为止。

此示例逐步讲解如何在用户按 Ctrl+S 或 F 时将 KeyDown 处理程序添加到 PodcastUserControl 以收藏播客。它使用与以前相同的命令。

PodcastUserControl.xaml.cs

// Respond to the F and Ctrl+S keys to favorite the focused item.
protected override void OnKeyDown(KeyRoutedEventArgs e)
{
    var ctrlState = CoreWindow.GetForCurrentThread().GetKeyState(VirtualKey.Control);
    var isCtrlPressed = (ctrlState & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down || (ctrlState & CoreVirtualKeyStates.Locked) == CoreVirtualKeyStates.Locked;

    if (e.Key == Windows.System.VirtualKey.F || (e.Key == Windows.System.VirtualKey.S && isCtrlPressed))
    {
        // Favorite the item using the defined command
        var favoriteCommand = Application.Current.Resources["favoriteCommand"] as ICommand;
        favoriteCommand.Execute(PodcastObject);
    }
}

鼠标加速器

将鼠标悬停在项上以显示按钮

用户熟悉右键单击上下文菜单,但你可能希望让用户仅使用鼠标单击一次即可执行常用命令。 若要启用此体验,可以在集合项的画布上包括专用按钮。 若要使用户能够快速使用鼠标执行作,并最大程度地减少视觉混乱,可以选择仅在用户在特定列表项中具有指针时显示这些按钮。

在此示例中,Favorite 命令由直接在 PodcastUserControl 中定义的按钮表示。 请注意,此示例中的按钮使用与之前相同的命令 FavoriteCommand。 若要切换此按钮的可见性,可以使用 VisualStateManager 在指针进入和退出控件时在视觉状态之间切换。

PodcastUserControl.xaml

<UserControl>
    <UserControl.ContextFlyout>
        <!-- ... -->
    </UserControl.ContextFlyout>
    <Grid Margin="12,0,12,0">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="HoveringStates">
                <VisualState x:Name="HoverButtonsShown">
                    <VisualState.Setters>
                        <Setter Target="hoverArea.Visibility" Value="Visible" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="HoverButtonsHidden" />
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <StackPanel>
            <TextBlock Text="{x:Bind PodcastObject.Title, Mode=OneWay}" Style="{StaticResource TitleTextBlockStyle}" />
            <TextBlock Text="{x:Bind PodcastObject.Description, Mode=OneWay}" Style="{StaticResource SubtitleTextBlockStyle}" />
            <TextBlock Text="{x:Bind PodcastObject.IsFavorite, Mode=OneWay}" Style="{StaticResource SubtitleTextBlockStyle}"/>
        </StackPanel>
        <Grid Grid.Column="1" x:Name="hoverArea" Visibility="Collapsed" VerticalAlignment="Stretch">
            <AppBarButton Icon="OutlineStar" Label="Favorite" Command="{StaticResource favoriteCommand}" CommandParameter="{x:Bind PodcastObject, Mode=OneWay}" IsTabStop="False" VerticalAlignment="Stretch"  />
        </Grid>
    </Grid>
</UserControl>

鼠标进入和退出项目时,悬停按钮应显示并消失。 若要响应鼠标事件,可以使用 PodcastUserControl 上的 PointerEnteredPointerExited 事件。

PodcastUserControl.xaml.cs

protected override void OnPointerEntered(PointerRoutedEventArgs e)
{
    base.OnPointerEntered(e);

    // Only show hover buttons when the user is using mouse or pen.
    if (e.Pointer.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Mouse || e.Pointer.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Pen)
    {
        VisualStateManager.GoToState(this, "HoverButtonsShown", true);
    }
}

protected override void OnPointerExited(PointerRoutedEventArgs e)
{
    base.OnPointerExited(e);

    VisualStateManager.GoToState(this, "HoverButtonsHidden", true);
}

悬停状态中显示的按钮只能通过指针输入类型进行访问。 由于这些按钮仅限于指针输入,因此可以选择最小化或删除按钮图标周围的填充,以优化指针输入。 如果选择这样做,请确保按钮占用空间至少为 20x20px,以使用笔和鼠标保持可用。

触摸加速器

Swipe

轻扫项目以显示命令

轻扫命令是一种触摸加速器,使触摸设备上的用户能够使用触摸执行常见的辅助作。 轻扫使触摸用户能够快速而自然地与内容进行交互,常见操作包括轻扫以删除或轻扫以调用。 有关详细信息,请参阅 轻扫命令 文章。

若要将轻扫集成到集合中,需要两个组件:SwipeItems,用于托管命令;和 SwipeControl,它包装项目并允许轻扫交互。

SwipeItems 可以定义为 PodcastUserControl 中的资源。 在此示例中,SwipeItems 包含了一个将项目添加到收藏夹的命令。

<UserControl.Resources>
    <SymbolIconSource x:Key="FavoriteIcon" Symbol="Favorite"/>
    <SwipeItems x:Key="RevealOtherCommands" Mode="Reveal">
        <SwipeItem IconSource="{StaticResource FavoriteIcon}" Text="Favorite" Background="Yellow" Invoked="SwipeItem_Invoked"/>
    </SwipeItems>
</UserControl.Resources>

SwipeControl 封装控件,允许用户使用轻扫手势与其交互。 请注意,SwipeControl 将 SwipeItems 引用为其 RightItems。 当用户从右向左轻扫时,“收藏夹”项将显示。

<SwipeControl x:Name="swipeContainer" RightItems="{StaticResource RevealOtherCommands}">
   <!-- The visual state groups moved from the Grid to the SwipeControl, since the SwipeControl wraps the Grid. -->
   <VisualStateManager.VisualStateGroups>
       <VisualStateGroup x:Name="HoveringStates">
           <VisualState x:Name="HoverButtonsShown">
               <VisualState.Setters>
                   <Setter Target="hoverArea.Visibility" Value="Visible" />
               </VisualState.Setters>
           </VisualState>
           <VisualState x:Name="HoverButtonsHidden" />
       </VisualStateGroup>
   </VisualStateManager.VisualStateGroups>
   <Grid Margin="12,0,12,0">
       <Grid.ColumnDefinitions>
           <ColumnDefinition Width="*" />
           <ColumnDefinition Width="Auto" />
       </Grid.ColumnDefinitions>
       <StackPanel>
           <TextBlock Text="{x:Bind PodcastObject.Title, Mode=OneWay}" Style="{StaticResource TitleTextBlockStyle}" />
           <TextBlock Text="{x:Bind PodcastObject.Description, Mode=OneWay}" Style="{StaticResource SubtitleTextBlockStyle}" />
           <TextBlock Text="{x:Bind PodcastObject.IsFavorite, Mode=OneWay}" Style="{StaticResource SubtitleTextBlockStyle}"/>
       </StackPanel>
       <Grid Grid.Column="1" x:Name="hoverArea" Visibility="Collapsed" VerticalAlignment="Stretch">
           <AppBarButton Icon="OutlineStar" Command="{StaticResource favoriteCommand}" CommandParameter="{x:Bind PodcastObject, Mode=OneWay}" IsTabStop="False" LabelPosition="Collapsed" VerticalAlignment="Stretch"  />
       </Grid>
   </Grid>
</SwipeControl>

当用户轻扫以调用 Favorite 命令时,将调用 Invoked 方法。

private void SwipeItem_Invoked(SwipeItem sender, SwipeItemInvokedEventArgs args)
{
    // Favorite the item using the defined command
    var favoriteCommand = Application.Current.Resources["favoriteCommand"] as ICommand;
    favoriteCommand.Execute(PodcastObject);
}

下拉以刷新

使用“下拉刷新”功能,用户可以通过触摸下拉数据集,以便检索更多数据。 请参阅 “下拉刷新” 一文以了解详细信息。

笔加速器

笔输入类型提供指针输入的精度。 用户可以执行常见操作,例如使用基于笔的加速器来打开上下文菜单。 若要打开上下文菜单,用户可以在按住侧按钮的状态下触摸屏幕,或长按内容。 用户还可以使用笔将鼠标悬停在内容上,以便更深入地了解 UI,例如显示工具提示,或显示类似于鼠标的辅助悬停作。

若要针对笔输入优化应用,请参阅 笔和触笔交互 文章。

Recommendations

  • 确保用户可以从所有类型的 Windows 设备访问所有命令。
  • 包括一个上下文菜单,该菜单提供对可用于集合项的所有命令的访问权限。
  • 为常用命令提供输入加速器。
  • 使用 ICommand 接口 实现命令。