Search code examples
wpfdata-binding

How to databind from a data template in DataGridRow


I have a DataGrid with the following setup:

<DataGrid
    x:Name="AperturesDataGrid"
    ItemsSource="{Binding Snapshots, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    RowStyle="{StaticResource DefaultRowStyle}"
    CellStyle="{StaticResource ContentDataGridCentering}"
    VerticalScrollBarVisibility="Auto"
    HorizontalScrollBarVisibility="Hidden"
    CanUserAddRows="False"
    CanUserResizeColumns="False"
    CanUserSortColumns="True"
    IsReadOnly="False"
    SelectionMode="Extended"
    SelectionUnit="FullRow"
    RowHeight="24"
    VerticalContentAlignment="Stretch"
    Margin="10">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Name"
                            Binding="{Binding Name, Mode=OneWay}"
                            Width="*"
                            CanUserSort="True"
                            SortMemberPath="Name"
                            IsReadOnly="True" />
        <DataGridTextColumn Header="Description"
                            Binding="{Binding Description, Mode=OneWay}"
                            Width="*"
                            CanUserSort="True"
                            SortMemberPath="Name"
                            IsReadOnly="True" />
        <DataGridTemplateColumn Header="Actions"
                                Width="100"
                                HeaderStyle="{StaticResource CenterGridHeaderStyle}">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Grid x:Name="RoomNameGrid"
                              HorizontalAlignment="Stretch"
                              Background="Transparent">
                        <StackPanel Orientation="Horizontal"
                                        HorizontalAlignment="Center">
                            <btn:DropDownButton
                                    MainImage="/Pollination.Core;Component/Resources/16x16/export.png"
                                    HoverImage="/Pollination.Core;Component/Resources/16x16/exportHover.png"
                                    VerticalAlignment="Center"
                                    Cursor="Hand"
                                    Width="15"
                                    Height="15"
                                    Margin="5,0,1,0"
                                    ToolTip="">
                                <btn:DropDownButton.Menu>
                                    <ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
                                        <MenuItem Command="{Binding ExportHb}"
                                                  CommandParameter="{Binding }"
                                                  Header='Honeybee Model *.hbjson'/>
                                    </ContextMenu>
                                </btn:DropDownButton.Menu>
                            </btn:DropDownButton>
                        </StackPanel>
                    </Grid>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

How do I bind the MenuItem to a Command that exists as the DataContext.ExportHb on the Window, and then pass the underlying object of the DataGridRow as the parameter? I am getting Binding Expression errors now.

BindingExpression path error: 'ExportHb' property not found on 'object' ''SnapshotWrapper' (HashCode=-1805182346)'. BindingExpression:Path=ExportHb; DataItem='SnapshotWrapper' (HashCode=-1805182346); target element is 'MenuItem' (Name=''); target property is 'Command' (type 'ICommand')

SnapshotWrapper is the object that each row is bound to. Any help would be appreciated. I obviously tried the relative source binding and binding to element name but that didn't work.


Solution

  • Routed events and relative bindings are resolved by different engines and they traverse the element tree differently.

    Routed events (which includes routed commands, as they are technically routed events) are controlled by the dependency property system and traverse a composite tree that is a hybrid of the visual and logical tree. This allows the event to route between disconnected visual trees. For example, a click raised inside a Popup or ContextMenu can be handled on a logical parent e.g., the element tree's root like a Window.

    The binding engine traverses the visual tree in order to find an ancestor data source. In the case of the ContextMenu or the Popup the visual tree of those elements is disconnected from the main visual tree. A ContextMenu is always hosted inside a Popup, hence they share the same behavior. That is that the content of a Popup.Child is hosted in its own Window instance. A Window always creates its own visual tree. Still that Window is logically connected to the element that owns it. The binding engine will not bridge that two individual visual trees the dependency property engine is doing.

    The solution to your problem is to use routed commands or routed events to trigger a parent element to execute the menu item's command action.

    Another solution could be to implement the ICommand on the data item, so that the DataGridColumn can bind to it (as the data item is the only available data context in your case, which is inherited by the ContextMenu). However, if the command action does not operate on the particular data item or requires the parent view model to execute the command action, then implementing the ICommand on those data items can be considered bad class design.

    Because you pass the data item as command parameter to the command chances are high that the command operates within the scope of the data item. If the command implementation does not require a reference to the parent view model class to execute, you should prefer to implement the ICommand in the data item instead of using routed commands. In this case, the MenuItem could directly bind to the ICommand that is now defined by the DataContext of the ContextMenu (the row's data item).

    Because implementing the ICommand on the data item is self-explanatory and only feasible in special use cases and you were not talking about what the command is doing I will give an example on the general approach that depends on routed commands and that can always be used.

    An advantage of routed commands is that you can associate them with keyboard shortcuts or mouse gestures.

    MainWindow.xaml

    <Window>
      <btn:DropDownButton>
        <btn:DropDownButton.Menu>
      
          <!-- 
               The DataContext is automatically inherited from the parent owner.
               In case of your example, the DataContext is the row data item itself.  
          -->
          <ContextMenu>
            <MenuItem Command="{x:Static MainWindow.ExportHbCommand}"
                      CommandParameter="{Binding}"
                      Header='Honeybee Model *.hbjson' />
          </ContextMenu>
        </btn:DropDownButton.Menu>
      </btn:DropDownButton>
    </Window>
    

    MainWindow.xaml.cs
    Based on the context, the example assumes that ExportHb is the ICommand exposed by your view model (because the property name does not indicate that it returns an ICommand, for example ExportHbCommand - instead the grammar indicates a method).

    partial class MainWindow : Window
    {
      public static RoutedUICommand ExportHbCommand { get; } = new RoutedUICommand(
        "Export Hb model",
        nameof(MainWindow.ExportHbCommand),
        typeof(MainWindow));
    
      private MainViewModel MainViewModel { get; }
    
      public MainWindow()
      {
        InitializeComponent();
    
        this.MainViewModel = new MainViewModel();
        this.DataContext = this.MainViewModel;
    
        // Register the command handlers.
        // Alternatively, use XAML to register them.  
    
        var exportHbCommandBinding = new CommandBinding(
          MainWindow.ExportHbCommand, 
          ExecutedExportHbCommand, 
          CanExecuteExportHbCommand);
        _ = this.CommandBindings.Add(exportHbCommandBinding);  
      }
    
      private void CanExecuteExportHbCommand(object sender, CanExecuteRoutedEventArgs e) 
        => e.CanExecute = this.MainViewModel.ExportHb.CanExecute(e.CommandParameter);
       
      private void ExecutedExportHbCommand(object sender, ExecutedRoutedEventArgs e)
        => this.MainViewModel.ExportHb.Execute(e.CommandParameter);
    }