0
votes

I've got a Treeview setup in a WPF (trying to mostly follow MVVM) application with a HierarchicalDataTemplate setup...for each item I've got a TextBlock and a couple buttons, as such:

<HierarchicalDataTemplate.ItemTemplate>
    <DataTemplate>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="13"/>
                <ColumnDefinition Width="13"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="20" />
            </Grid.RowDefinitions>
            <TextBlock Text="{Binding Name}" Grid.Column="0">
                <TextBlock.ContextMenu>
                    <ContextMenu DataContext="{Binding Path=PlacementTarget.DataContext, RelativeSource={RelativeSource Mode=Self}}">
                        <MenuItem Header="Delete Document" ToolTip="{Binding IDPath}" Command="{Binding DeleteDocumentCommand}"
                                                                                        CommandParameter="{Binding IDPath}" />
                    </ContextMenu>
                </TextBlock.ContextMenu>
            </TextBlock>
            <Button Content="X" Grid.Column="1" Height="12" Width="12" HorizontalAlignment="Center" ToolTip="Delete Document" Command="{Binding DataContext.DeleteDocumentCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" CommandParameter="{Binding IDPath}"  />
            <Button Content="X" Grid.Column="2" Height="12" Width="12" HorizontalAlignment="Center" ToolTip="Move Document" Command="{Binding DataContext.MoveDocumentCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" CommandParameter="{Binding IDPath}" />
        </Grid>
    </DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>

All of these bindings are working well, except for the Command on the context menu MenuItem. I have looked through several solutions here on SO (above is one attempt, I also tried to set the "Tag" of the parent ...which also did not help (using PlacementTarget.Tab instead of DataContext).

I must be missing something simple but I just can't figure it out. I setup a binding to the 's Tooltip property, just to verify that the IDPath is bound correct (it is)...but the DeleteDocumentCommand just refuses to fire.

If anybody has any ideas I'd appreciate it, this has been killing me for the last few hours...thanks!

Edit, here's an example with the "Tag" concept to kind of transfer the DataContext from an object in the viewtree (The TextBlock) and make it accessible to the ContextMenu. With this method, again, everything binds fine (MenuItem->Tooltip for example), but still the method won't fire...

<HierarchicalDataTemplate.ItemTemplate>
    <DataTemplate>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="13"/>
                <ColumnDefinition Width="13"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="20" />
            </Grid.RowDefinitions>
            <TextBlock Text="{Binding Name}" Grid.Column="0" Tag="{Binding DataContext, RelativeSource={RelativeSource Self}}">
                <TextBlock.ContextMenu>
                    <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                        <MenuItem Header="Delete Document" ToolTip="{Binding IDPath}" Command="{Binding DeleteDocumentCommand}"
                                                                                        CommandParameter="{Binding IDPath}" />
                    </ContextMenu>
                </TextBlock.ContextMenu>
            </TextBlock>
            <Button Content="X" Grid.Column="1" Height="12" Width="12" HorizontalAlignment="Center" ToolTip="Delete Document" Command="{Binding DataContext.DeleteDocumentCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" CommandParameter="{Binding IDPath}"  />
            <Button Content="X" Grid.Column="2" Height="12" Width="12" HorizontalAlignment="Center" ToolTip="Move Document" Command="{Binding DataContext.MoveDocumentCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" CommandParameter="{Binding IDPath}" />
        </Grid>
    </DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>

My instincts tell me that since the Tooltip binding it working, that I am successfully getting the DataContext down to the ContextMenu level...and yet, the Command won't fire...I'm not sure what else I'm missing here.

Edit Again: Ok...I see now that the DataContext that I'm gaining access via the "Tag" attribute is the collection that is populating the treeview...not the actual ViewModel object itself...so I'm assuming that's why the command doesn't work. So I'm not sure how to get the correct DataContext at this point...should it be coming from the top-most window?

1
As a temporary work-around, I tested handling the Click event in the code behind (not very MVVM-ish, I know)....but I can see from the 'sender' of the event that the CommandParameter is set correctly, but the Command is null. If I give myself a reference to the view model in the view code-behind (yea, I know I know...terrible), I can execute the command (ICommand property) passing the sender.CommandParameter and it works fine. I have no idea why the Command isn't being set correctly in the XAML though.barbrady
1) click edit 2) select code 3) ctrl-k until code is indented four spaces rather than fourtyuser1228

1 Answers

1
votes

(Updated)

The Tag of TextBlock can be bound to the parent TreeView which its DataContext is the ViewModel. The Tag of the Context menu can be bound to this Tag (again). You can now use

PlacementTarget.Tag.DataContext. `Your Command`

in other words:

<TreeView DataContext="{Binding}" ItemsSource="{Binding Models}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Models}">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="13"/>
                    <ColumnDefinition Width="13"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="20" />
                </Grid.RowDefinitions>
                <TextBlock Text="{Binding Name}" Grid.Column="0" Tag="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TreeView}}">
                    <TextBlock.ContextMenu>
                        <ContextMenu Tag="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                            <MenuItem Header="Delete Document" ToolTip="{Binding Name}" 
                                              Command="{Binding PlacementTarget.Tag.DataContext.PCommand, 
                                                        RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}"    
                                              CommandParameter="{Binding Name}" />
                        </ContextMenu>
                    </TextBlock.ContextMenu>
                </TextBlock>
            </Grid>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
 </TreeView>

Regarding the DataContext, you should set it to be the ViewModel, But not the ObservableCollection in the ViewModel. something like this:

    public MainWindow()
    { 
        InitializeComponent();

        ViewModel mv = new ViewModel();
        this.DataContext = mv;
    }

There are other options that you can Google. Note that the ViewModel/Model/Commad classes are:

public class ViewModel :INotifyPropertyChanged
{
    public ViewModel()
    { 
        _pCommand = new Command(DoParameterisedCommand);


        Model m1 = new Model() { Name = "model1" };
        Model m2 = new Model() { Name = "model2" };
        Model m3 = new Model() { Name = "model3" };
        Model m4 = new Model() { Name = "model4", Models = new Model[2] { m1, m2 } };
        Model m5 = new Model() { Name = "model5", Models = new Model[2] { m4, m3 } };
        Model m6 = new Model() { Name = "model6" };
        Model m7 = new Model() { Name = "model7" };
        Model m8 = new Model() { Name = "model8" };
        Model m9 = new Model() { Name = "model9", Models = new Model[2] { m6, m7 } };
        Model m10 = new Model() { Name = "model10", Models = new Model[2] { m8, m9 } };
        _models = new ObservableCollection<Model>(new Collection<Model>() { m1, m2, m3, m4, m5 });

    }

    ObservableCollection<Model> _models;
    public ObservableCollection<Model> Models { get { return _models; } set { _models = value; RaisePropertyChanged("Models"); } }


    private void DoParameterisedCommand(object parameter)
    {
        MessageBox.Show("Parameterised Command; Parameter is '" +
                     parameter.ToString() + "'.");
    }
    Command _pCommand;
    public Command PCommand
    {
        get { return _pCommand; }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(string propname)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
    }
}

public class Model : INotifyPropertyChanged
{
    Model[] _models;
    public Model[] Models { get { return _models; } set { _models = value; RaisePropertyChanged("Models"); } }

    string _name;
    public string Name { get { return _name; } set { _name = value; RaisePropertyChanged("Name"); } }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(string propname)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
    }
}

public class Command : ICommand
{
    public Command(Action<object> parameterizedAction, bool canExecute = true)
    {
        _parameterizedAction = parameterizedAction;
        _canExecute = canExecute;
    }

    Action<object> _parameterizedAction = null;
    bool _canExecute = false;

    public bool CanExecute
    {
        get { return _canExecute; }
        set
        {
            if (_canExecute != value)
            {
                _canExecute = value;
                CanExecuteChanged?.Invoke(this, EventArgs.Empty);
            }
        }
    }
    public event EventHandler CanExecuteChanged;
    bool ICommand.CanExecute(object parameter)
    {
        return _canExecute;
    }

    void ICommand.Execute(object parameter)
    {
        this.DoExecute(parameter);
    }
    public virtual void DoExecute(object param)
    {
        if (_parameterizedAction != null)
            _parameterizedAction(param);
        else
            throw new Exception();
    }
 }