Click here to Skip to main content
14,838,162 members
Please Sign up or sign in to vote.
5.00/5 (1 vote)
See more: , +
I've been learning C# for years, and finally decided to learn and use WPF and MVVM, and no code-behind. This is my first app, and for the most part, it's going rather well. I haven't been able to find an answer to the following problem.

I have a simple application where I have a WPF executable front end and a DLL class library back end. The front end has a DataGrid that is bound to a DataTable in a ViewModel class; which I populate from a query to an SQL 2000 database when the user requests it. I'd like to be able to let the user right-click on a row in a DataGrid and display a ContextMenu that when clicked, opens a browser and displays information related to the ticket for the row that was clicked. That ContextMenu item is a command in the ViewModel class that starts a process that opens a URL and uses text from one of the columns for the DataRowView for the specific item that was right-clicked. Aside from not being sure if I should operate off the SelectedItem or SelectedIndex to reference directly with the DataTable (perhaps there could be a time when the indexes are different?)--for the most part, that's working with roughly the following code:

Some of the XAML:
XML
<DataGrid
    x:Name="TicketsDataGrid"
    ItemsSource="{Binding TicketsViewModel.TicketsDataTable}"
    AutoGenerateColumns="True"
    CanUserAddRows="False"
    CanUserSortColumns="True"
    CanUserResizeRows="False"
    IsReadOnly="True"
    SelectionMode="Single"
    SelectionUnit="FullRow">
    <DataGrid.ContextMenu>
        <ContextMenu>
            <MenuItem
                Header="View Ticket in browser"
                Command="{Binding Path=TicketsViewModel.ViewTicketInBrowser_Command}"
                CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}, Path=PlacementTarget.SelectedItem}"/>
        </ContextMenu>
    </DataGrid.ContextMenu>
</DataGrid>


Part of TicketsViewModel:
C#
private RelayCommand viewTicketInBrowser_Command;

public ICommand ViewTicketInBrowser_Command
{
    get
    {
        return this.viewTicketInBrowser_Command ?? (this.viewTicketInBrowser_Command = new RelayCommand(this.ViewTicketInBrowser_Command_Execute, param => ViewTicketInBrowser_Command_CanExecute()));
    }
}

public bool ViewTicketInBrowser_Command_CanExecute()
{
    return true;
}

public void ViewTicketInBrowser_Command_Execute(object ticketObject)
{
    if (ticketObject != null)
    {
        if (ticketObject.GetType() == typeof(System.Data.DataRowView) && ((System.Data.DataRowView)ticketObject).Row.Table.Columns.Contains("TicketNumber"))
        {
            System.Diagnostics.Process.Start("http://mysite/ticket_detail.asp?ticketnum=" + ((System.Data.DataRowView)ticketObject).Row.Field<string>("TicketNumber"));
        }
    }
}


The above works, but will allow right-click and the ContextMenu anywhere in the DataGrid. I only want it to be available if you right-click on a row in the DataGrid. Ideally I'd like to get the row that was clicked, however only know how to get the SelectedItem or SelectedIndex. So I figured if I limited to just allowing the ContextMenu on the row, that could be the best option. I can see where a ContextMenu would be good for many things on the DataGrid itself; but for just the row, it seemed odd to let it happen just anywhere on the DataGrid.

What I have tried:

I tried several ideas from searches, such as defining the ContextMenu in the DataGrid.Resources area, then setting a DataGrid.RowStyle, but I don't seem to have that right. It seems to limit the ContextMenu to just the row, but I cant seem to wire it correctly to my command.

Alternate XAML I tried:
XML
<DataGrid.Resources>
    <ContextMenu
        x:Key="RowMenu"
        DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
        <MenuItem
            Header="View Ticket in browser"
            Command="{Binding Path=TicketsViewModel.ViewTicketInBrowser_Command}"
            CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=PlacementTarget.SelectedItem}" />
    </ContextMenu>
</DataGrid.Resources>
<DataGrid.RowStyle>
    <Style TargetType="DataGridRow">
        <Setter Property="ContextMenu" Value="{StaticResource RowMenu}" />
    </Style>
</DataGrid.RowStyle>


Have tried several variants and tweaks, but seem to get close to the same two errors in Output:

Error 1:
System.Windows.Data Error: 40 : BindingExpression path error: 'TicketsViewModel' property not found on 'object' ''DataRowView' (HashCode=7742767)'. BindingExpression:Path=TicketsViewModel.ViewTicketInBrowser_Command; DataItem='DataRowView' (HashCode=7742767); target element is 'MenuItem' (Name=''); target property is 'Command' (type 'ICommand')

Error 2:
System.Windows.Data Error: 40 : BindingExpression path error: 'PlacementTarget' property not found on 'object' ''DataGrid' (Name='TicketsDataGrid')'. BindingExpression:Path=PlacementTarget.SelectedItem; DataItem='DataGrid' (Name='TicketsDataGrid'); target element is 'MenuItem' (Name=''); target property is 'CommandParameter' (type 'Object')

I think this is related to the main issue where the Ancestor bindings don't work, as the context menu is not a child of the element it is on; being the DataGrid. The Stack Overflow article "How to set a binding in WPF Toolkit Datagrid's ContextMenu CommandParameter" led me to think that.

Perhaps I've had too much coffee, or have been staring at the screen too long; but need some assistance figuring this out.
Posted
Updated 23-May-16 8:31am
Comments
Kailash Polai 23-May-16 2:08am
   
Can you first fix the binding errors. Also can you try setting contextmenu with the CellStyle

Here's the final solution I ended up with.

Final XAML that works:
XML
<DataGrid.Resources>
    <ContextMenu x:Key="ticketContextMenu">
	    <ContextMenu.Items>
            <MenuItem
                Header="Open ticket in browser"
                Command="{Binding DataContext.TicketsViewModel.ViewTicketInBrowser_Command, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}"
                CommandParameter="{Binding DataContext.TicketNumber, RelativeSource={RelativeSource Mode=Self}}"/>
        </ContextMenu.Items>
    </ContextMenu>
</DataGrid.Resources>
<DataGrid.RowStyle>
    <Style TargetType="DataGridRow" BasedOn="{StaticResource {x:Type DataGridRow}}">
        <Setter Property="ContextMenu" Value="{StaticResource ticketContextMenu}" />
    </Style>
</DataGrid.RowStyle>


With guidance from Kassim's response, and a little searching; I had to tweak the alternate XAML code I provided to get it to work. I think the key was specifying DataContext when binding, which I think was a piece that was breaking the previous code I tried. I also made a couple other changes. Either way, I only get the ContextMenu when I right click on a row; and operates correctly when I click it for the appropriate Command and providing the CommandParameters related to the specified column in the DataGrid.

Also based on guidance from Kassim's response, it showed me how to reference the DataGrid column value directly. I like sending the TicketNumber string to the command instead of the SelectedItem or SelectedIndex; though am not sure if that's the best way I should be doing it (i.e. sending the entire row, or just the text). A question for a another thread though.
   
v2
Define ur context menu in resource as below :
HTML
<ContextMenu x:Key="menu">
           <ContextMenu.Items>
               <MenuItem Header="{Binding DataContext.Column1,RelativeSource={RelativeSource Mode=Self}}"  Click="MenuItem_Click" />
               <MenuItem Header="{Binding DataContext.Column2,RelativeSource={RelativeSource Mode=Self}}" />
               <MenuItem Header="{Binding DataContext.Column3,RelativeSource={RelativeSource Mode=Self}}" />
           </ContextMenu.Items>
       </ContextMenu>


For simplicity i am using click event instead of command. What i am doing is binding the column values of each row to context menu.

Use the above context menu as a static resource in datagrid row style. The binding of each header will be resolved to actual content from your DataModel.


Instead of click event. Use appropriate command from the view model. The relative source property in binding is use to resolve the instance of view model.

I hope this solves your problem
   
Comments
bnmc 23-May-16 13:41pm
   
Thanks for the response Kassim. Your answer provided good insight, and ultimately pushed me in the right direction for the final answer. I don't plan to use any code-behind, so thanks for the reference to "use appropriate command from the view model," otherwise that would have been confusing.

I was still confused though, as what you reference was like what I provided as the XAML I tried already; just not able to figure out how to reference things correctly. I found though that I needed to use "DataContext" in a couple more places to get things to reference correctly. What you show for the header and using DataContext.Column to reference the value directly, gave me the "a-ha" moment. With a little searching, I was able to use elements of what you suggested, examples online, and a little tweaking; to find the final answer. I'll post it as a separate solution, so I can mark up the XAML for easier viewing.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)




CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900