为数据网格行创建上下文菜单

18

我有一个数据网格,潜在地可能有许多行。当用户右键单击其中一行时,我需要为每行显示一个上下文菜单,并在用户单击选项时执行一个操作(根据当前选定的行选择相同的操作但不同的数据项)。

针对这个问题,什么是最佳策略?

虽然我使用ContextMenuOpening事件创建菜单,从某种意义上来说它像一个“延迟加载”的上下文菜单,但我担心为每一行都创建一个ContextMenu会过度冗余。那我应该只为datagrid使用一个ContextMenu吗?但这样一来,我需要更多的工作来确定正确的行和点击事件等细节。

1个回答

45
据我所知,一些操作会因行而异,因此为 DataGrid 设计单个 ContextMenu 没有意义。
我有一个基于行的上下文菜单示例。
<UserControl.Resources>
    <ContextMenu  x:Key="RowMenu" DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
        <MenuItem Header="Edit" Command="{Binding EditCommand}"/>
    </ContextMenu>
    <Style x:Key="DefaultRowStyle" TargetType="{x:Type DataGridRow}">
        <Setter Property="ContextMenu" Value="{StaticResource RowMenu}" />
    </Style>
</UserControl.Resources>

<DataGrid RowStyle="{StaticResource DefaultRowStyle}"/>

DataGrid 必须绑定到一个带有命令的视图模型列表:

public class ItemModel
{
    public ItemModel()
    {
        this.EditCommand = new SimpleCommand 
        { 
            ExecuteDelegate = _ => MessageBox.Show("Execute"), 
            CanExecuteDelegate = _ => this.Id == 1 
        };
    }
    public int Id { get; set; }
    public string Title { get; set; }
    public ICommand EditCommand { get; set; }
}

上下文菜单是在UserControl的资源集合中创建的,我认为只有一个对象通过引用而不是值连接到数据格行。

这是另一个关于MainViewModelCommandContextMenu的例子。我假设DataGrid具有正确的视图模型作为DataContext,并且CommandParameter属性必须放置在Command属性之前:

    <ContextMenu  x:Key="RowMenu" DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
        <MenuItem Header="Edit" CommandParameter="{Binding}"
                  Command="{Binding DataContext.DataGridActionCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}" />
    </ContextMenu>

模型:

public class MainViewModel
{
    public MainViewModel()
    {
        this.DataGridActionCommand = new DelegateCommand<ItemModel>(m => MessageBox.Show(m.Title), m => m != null && m.Id != 2);
    }

    public DelegateCommand<ItemModel> DataGridActionCommand { get; set; }
    public List<ItemModel> Items { get; set; }
}

public class ItemModel
{
    public int Id { get; set; }
    public string Title { get; set; }
}

但是有一个问题,如果CanExecute返回false,则MenuItem不会显示为禁用项。可能的解决方法是在ItemModel内部使用ParentModel属性,但它与第一种解决方案并没有太大区别。 以下是上述解决方案的示例:

public class ItemModel
{
    public int Id { get; set; }
    public string Title { get; set; }
    public MainViewModel ParentViewModel { get; set; }
}

//Somewhere in the code-behind, create the main view model 
//and force child items to use this model as a parent model
var mainModel = new MainViewModel { Items = items.Select(item => new ItemViewModel(item, mainModel)).ToList()};

在XAML中,MenuItem会更简单:

<MenuItem Header="Edit" CommandParameter="{Binding}"
              Command="{Binding ParentViewModel.DataGridActionCommand}" />

谢谢提供示例,我会尝试一下。但是,和你一样,我也有同样的疑问,是否只有一个上下文菜单对象?我需要深入研究一下。 - Jay
我测试了一下,比较了每行 ContextMenus 的哈希码,发现它们提供了相同的值,因此应该是同一个对象。不错!但是,我真的需要为每一行都创建一个 Command 吗?我想使用像父级 Command 一样的东西,它位于顶部,内部包含主 ViewModel。只需调整 MenuItem 中的 Command 绑定,就可以实现这个功能吗?我试了好几次都没成功!该死... - Jay
在我的情况下,我有MainView和相应的MainViewModel(其中包含Prism的DelegateCommand的实例称为DataGridActionCommand)。 Main View具有显示上下文菜单的行为,当用户右键单击每一行时(DataItem是ItemViewModel),会出现此操作。我希望每行都能从MainViewModel执行DataGridActionCommand(并将其传递给DataItem),而不是在行视图模型上执行命令。这有帮助吗?如果您想要,我可以发布部分方案的代码以更好地理解。 - Jay
对我来说可以。感谢您的努力。为了结束这个话题,有没有更好的架构方式呢?例如,像您在第一次回复中描述的那样? - Jay
@Jay 我已经在我的答案中添加了两个代码块。虽然模型看起来很奇怪,但是这段代码并没有违反MVVM的原则,并且它也可以在Silverlight中工作。 - vortexwolf
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接