18

I have a datagrid that potentially can have many rows. As the user right clicks one of the rows, I need to show a context menu for each of the rows and perform an action (same action but different data item according to the current selected row) when the user clicks the option.

What is the best strategy for this?

I'm fearing that a ContextMenu for each row is overkill even though I'm creating the menu using the ContextMenuOpening event, sort of a "lazy load" for the context menu. Should I only use one ContextMenu for the datagrid? But with this I would have some more work regarding the click event, to determine the correct row, etc.

Christian
  • 1,017
  • 3
  • 14
  • 30
Jay
  • 259
  • 1
  • 3
  • 10

1 Answers1

45

As far as I know, some of the actions will be disabled or enabled depending on the row, so there is no point in a single ContextMenu for a DataGrid.

I have an example of the row-level context menu.

<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}"/>

The DataGrid must have a binding to a list of view models with commands:

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; }
}

The context menu is created in the resources collection of the UserControl and I think there is only one object which is connected with datagrid rows by reference, not by value.

Here is another example of ContextMenu for a Command inside a MainViewModel. I suppose that DataGrid has a correct view model as the DataContext, also the CommandParameter attribute must be placed before the Command attribute:

    <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>

Models:

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; }
}

But there is a problem that MenuItem isn't displayed as a disabled item if CanExecute returns false. The possible workaround is using a ParentModel property inside the ItemModel, but it doesn't differ much from the first solution. Here is example of above-described solution:

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()};

And MenuItem in XAML will be simplier:

<MenuItem Header="Edit" CommandParameter="{Binding}"
              Command="{Binding ParentViewModel.DataGridActionCommand}" />
vortexwolf
  • 13,967
  • 2
  • 54
  • 72
  • Thanks for the sample, I'll give it a try. But, like you, I have the same doubt, is there only one Context Menu object? I'll have to dig into this. – Jay Mar 06 '11 at 11:32
  • I tested this, and comparing the Hash Code of the ContextMenus of each of the rows it provides the same value so it should be the same object.Nice! Do I really need a Command for each row? I wanted to use like a parent Command which sits at the top, inside the main ViewModel. Can this be done just by adjusting the Command Binding in the MenuItem? I can't get it to work! damn it... – Jay Mar 06 '11 at 13:45
  • @Jay Do you mean a static RoutedCommand? Actually it is possible to use event handlers or global commands instead of row-level commands. I will try to implement something similar tomorrow, and if you could add the code of your command to the question it would help me. – vortexwolf Mar 06 '11 at 20:00
  • In my scenario I have the MainView and the corresponding MainViewModel (which contains an instance of a Prism's DelegateCommand called DataGridActionCommand). The Main View has the datagrid with the behavior of showing the context menu as the user right clicks each row (DataItem is ItemViewModel). I wanted that each row would execute the DataGridActionCommand, from the MainViewModel (and pass it the DataItem) and not a Command on the rows View Model. Does this help? If you want I can post code for part of the scenario for better understanding. – Jay Mar 06 '11 at 22:00
  • @Jay I've edited my answer. The solution isn't ideal, but it works as you've described. – vortexwolf Mar 07 '11 at 20:04
  • Thanks.I'll give it a try tomorrow and provide some feedback. – Jay Mar 07 '11 at 22:54
  • Works for me. Thank you for your effort. Just to end this, is there a better way to architect this situation? For example, like you described in your first response? – Jay Mar 08 '11 at 22:55
  • @Jay I have added two blocks of code to my answer. Although models look strange, the code doesn't violate the principles of MVVM, and it works in Silverlight as well. – vortexwolf Mar 09 '11 at 08:40