Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF DataGrid ContextMenu Command binding to MVVMLight RelayCommand<T> not always working

I have a WPF DataGrid for which I want to add a ContextMenu using MVVM. This DataGrid resides in a UserControl (I removed a bunch of stuff which I don't think have to do with the essence of the problem):

<UserControl x:Class="Legend.MarkerMultiStatisticsControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:legend="clr-namespace:Legend"
             Name="Self"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <GroupBox Header="{Binding ElementName=Self, Path=StaisticsData.Title}">
        <DockPanel>
            <DataGrid 
                      DataContext="{Binding ElementName=Self}"
                      ItemsSource="{Binding ElementName=Self, Path=StaisticsData.Statistics}"
                      SelectionMode="Single"
                      CurrentCell="{Binding ElementName=Self, Path=CurrentCell, Mode=OneWayToSource}">
                <DataGrid.ContextMenu>
                    <ContextMenu DataContext="{Binding RelativeSource={RelativeSource Mode=Self}, Path=PlacementTarget.DataContext}"
                                 ItemsSource="{Binding Path=Commands}">
                        <ContextMenu.ItemContainerStyle>
                            <Style TargetType="MenuItem">
                                <Setter Property="Header" Value="{Binding Header}"></Setter>
                                <Setter Property="Command" Value="{Binding Command, diag:PresentationTraceSources.TraceLevel=High}"></Setter>
                                <Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}, Path=DataContext.CurrentLegendCell}"></Setter>
                            </Style>
                        </ContextMenu.ItemContainerStyle>
                    </ContextMenu>
                </DataGrid.ContextMenu>
            </DataGrid>
        </DockPanel>
    </GroupBox>
</UserControl>

This control has a DependencyProperty called Command to which I bind the menu's ItemsSource and its type is ObservableCollection<LegendCommand> with LegendCommand being

public class LegendCommand : ObservableObject
{
    private string _header;
    private ICommand _command;

    public string Header
    {
        get { return _header; }
        set { Set(()=>Header, ref _header, value); }
    }

    public ICommand Command
    {
        get { return _command; }
        set { Set(() => Command, ref _command, value); }
    }
}

These commands are generated in different places in my view models.

In one place (inside an Adapter class whose Commands are bound to the control's Commands) I have the following:

Commands.Add(new LegendCommand
            {
                Header = "From inside adapter...",
                Command = new RelayCommand<LegendCellInfo>(info =>
                {
                    MessageBox.Show(string.Format("State: {0}, Property: {1}", info.State, info.PropertyName));
                })
            });

And in a different place (the view model which holds the Adapter) I have the following:

adapter.Commands.Add(new LegendCommand
            {
                Header = "Select",
                Command = new RelayCommand<LegendCellInfo>(info =>
                {
                    // selection logic
                })
            });

My problem is that the "From inside adapter..." command is executed but "Select" isn't. I put a breakpoint in the "Select" code but it's never called.

The binding of the ContextMenu's ItemsSource to the Commands property works as I see both options in the menu. The binding of the ICommans's probably works because one of the commands is executed.

I debugged the code and binding and have the following info:

The Commands collection holds two elements whose hash codes are 40262542 and 26818564 (got them from running adapter.Commands[0].Command.GetHashCode() and adapter.Commands[1].Command.GetHashCode() in Visual Studio's Immediate Window).

The binding tracing gives the following output when I right-click the DataGrid:

System.Windows.Data Warning: 56 : Created BindingExpression (hash=11440639) for Binding (hash=54536677)
System.Windows.Data Warning: 58 :   Path: 'Command'
System.Windows.Data Warning: 60 : BindingExpression (hash=11440639): Default mode resolved to OneWay
System.Windows.Data Warning: 61 : BindingExpression (hash=11440639): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 62 : BindingExpression (hash=11440639): Attach to System.Windows.Controls.MenuItem.Command (hash=54276594)
System.Windows.Data Warning: 67 : BindingExpression (hash=11440639): Resolving source 
System.Windows.Data Warning: 70 : BindingExpression (hash=11440639): Found data context element: MenuItem (hash=54276594) (OK)
System.Windows.Data Warning: 78 : BindingExpression (hash=11440639): Activate with root item LegendCommand (hash=5940773)
System.Windows.Data Warning: 108 : BindingExpression (hash=11440639):   At level 0 - for LegendCommand.Command found accessor RuntimePropertyInfo(Command)
System.Windows.Data Warning: 104 : BindingExpression (hash=11440639): Replace item at level 0 with LegendCommand (hash=5940773), using accessor RuntimePropertyInfo(Command)
System.Windows.Data Warning: 101 : BindingExpression (hash=11440639): GetValue at level 0 from LegendCommand (hash=5940773) using RuntimePropertyInfo(Command): RelayCommand`1 (hash=40262542)
System.Windows.Data Warning: 80 : BindingExpression (hash=11440639): TransferValue - got raw value RelayCommand`1 (hash=40262542)
System.Windows.Data Warning: 89 : BindingExpression (hash=11440639): TransferValue - using final value RelayCommand`1 (hash=40262542)
System.Windows.Data Warning: 56 : Created BindingExpression (hash=31617173) for Binding (hash=54536677)
System.Windows.Data Warning: 58 :   Path: 'Command'
System.Windows.Data Warning: 60 : BindingExpression (hash=31617173): Default mode resolved to OneWay
System.Windows.Data Warning: 61 : BindingExpression (hash=31617173): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 62 : BindingExpression (hash=31617173): Attach to System.Windows.Controls.MenuItem.Command (hash=16119107)
System.Windows.Data Warning: 67 : BindingExpression (hash=31617173): Resolving source 
System.Windows.Data Warning: 70 : BindingExpression (hash=31617173): Found data context element: MenuItem (hash=16119107) (OK)
System.Windows.Data Warning: 78 : BindingExpression (hash=31617173): Activate with root item LegendCommand (hash=16131920)
System.Windows.Data Warning: 107 : BindingExpression (hash=31617173):   At level 0 using cached accessor for LegendCommand.Command: RuntimePropertyInfo(Command)
System.Windows.Data Warning: 104 : BindingExpression (hash=31617173): Replace item at level 0 with LegendCommand (hash=16131920), using accessor RuntimePropertyInfo(Command)
System.Windows.Data Warning: 101 : BindingExpression (hash=31617173): GetValue at level 0 from LegendCommand (hash=16131920) using RuntimePropertyInfo(Command): RelayCommand`1 (hash=26818564)
System.Windows.Data Warning: 80 : BindingExpression (hash=31617173): TransferValue - got raw value RelayCommand`1 (hash=26818564)
System.Windows.Data Warning: 89 : BindingExpression (hash=31617173): TransferValue - using final value RelayCommand`1 (hash=26818564)

If I understand correctly, we can see that two binding operations take place - the first results in binding to 'RelayCommand'1 (hash=40262542) and the second to 'RelayCommand'1 (hash=26818564) - incidentally these are the hash codes for my two commands.

Also, no exceptions or other errors occur.

I'm stuck in my investigation. Where else to look?

UPDATE 1: When I change the code in "Select" command to the following instead of the code I had before:

adapter.Commands.Add(new LegendCommand
            {
                Header = "Select",
                Command = new RelayCommand<LegendCellInfo>(info =>
                {
                    MessageBox.Show("yes");
                })
            });

then the code starts working all of a sudden...

UPDATE 2: The original command uses members of the Adapter class. Searching online for problems with MVVMLight RelayCommand and member functions found this SO question: RelayCommand with Argument throwing MethodAccessException (but obviously I want to continue using MVVMLight...)

UPDATE 3: Read the MVVMLight code for RelayCommand and it doesn't save a hard reference to the method so I think that nothing is keeping the lambda alive. Will refactor the code now and update here if it works.

like image 391
Dina Avatar asked Jan 15 '15 12:01

Dina


1 Answers

Indeed the reason is that the "Select" command was not kept alive by MVVMLight's weak reference. I wrote about it in more detail here: http://blogs.microsoft.co.il/dinazil/2015/01/16/relaycommands-weakfuncs/

like image 112
Dina Avatar answered Nov 15 '22 08:11

Dina