Invoke Command when TreeViewItem is Expanded
Asked Answered
W

1

6

Sounds simple enough? I have a TreeView, and I want something to happen when one of the nodes is expanded. I'm using MVVM, so that 'something' is a command in the ViewModel.

Well, I'm finding that it's not so simple after all. I've looked around and tried a few things. For example, using MVVM Light's EventToCommand:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="TreeViewItem.Expanded">
        <cmd:EventToCommand Command="{Binding Path=FolderNodeToggledCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

This code (based on this and this) doesn't work (nothing fires; the command is bound in the ViewModel but the corresponding method is never fired when a node is expanded). I've also tried replacing cmd:EventToCommand with i:InvokeCommandAction and the results are the same. The 'solution' in the second link is clearly overkill and I don't want to change the ToggleButton since I want to use the WPF TreeView WinForms Style which has its own ToggleButton. The secondary answer in the second link suggests that I might be attempting to use an event on TreeView that doesn't exist.

Another possible solution could be to bind the TreeViewItem's IsExpanded property. However I'd like to keep the objects I'm binding to as clean DTOs and perform an action in the ViewModel, not in the objects being bound.

So what will it take to invoke a command in the ViewModel when a TreeViewItem is expanded?

Wreath answered 26/4, 2014 at 21:55 Comment(4)
Yes, by all means please post an answer. I haven't used Prism but I'll see whether I can get it working.Wreath
Ok, I'll post an answer that gives a step-by-step, and we'll see if it's of any use.Astrict
Note: since the command is bound to a behaviour, you shouldn't inline the declaration tho'Astrict
Inline? How do you mean? Btw, thanks for the detailed answer - I'm going to try it out in the coming days, since I'm a little overwhelmed with other stuff at the moment.Wreath
A
9

To get this working, you can use an attached behaviour, and you'll see that it's a clean MVVM strategy.

Create a WPF app and add this Xaml...

<Grid>
    <TreeView>
        <TreeView.Resources>
            <Style TargetType="TreeViewItem">
                <Setter Property="bindTreeViewExpand:Behaviours.ExpandingBehaviour" Value="{Binding ExpandingCommand}"/>
            </Style>
        </TreeView.Resources>
        <TreeViewItem Header="this" >
            <TreeViewItem Header="1"/>
            <TreeViewItem Header="2"><TreeViewItem Header="Nested"></TreeViewItem></TreeViewItem>
            <TreeViewItem Header="2"/>
            <TreeViewItem Header="2"/>
            <TreeViewItem Header="2"/>
        </TreeViewItem>
        <TreeViewItem Header="that" >
            <TreeViewItem Header="1"/>
            <TreeViewItem Header="2"/>
            <TreeViewItem Header="2"/>
            <TreeViewItem Header="2"/>
            <TreeViewItem Header="2"/>
        </TreeViewItem>        
    </TreeView>
</Grid>

Then create a View Model like this...

public class ViewModel : INotifyPropertyChanged
{
    public ICommand ExpandingCommand { get; set; }
    public ViewModel()
    {
        ExpandingCommand = new RelayCommand(ExecuteExpandingCommand, CanExecuteExpandingCommand);
    }
    private void ExecuteExpandingCommand(object obj)
    {
        Console.WriteLine(@"Expanded");
    }
    private bool CanExecuteExpandingCommand(object obj)
    {
        return true;
    }
    #region INotifyPropertyChanged Implementation
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string name)
    {
        var handler = System.Threading.Interlocked.CompareExchange(ref PropertyChanged, null, null);
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
    #endregion
}

I use the Relay Command, but you can use the Delegate Command interchangeably. The source for the Relay Command is at http://msdn.microsoft.com/en-us/magazine/dd419663.aspx

Then create a separate class that looks like this...

public static class Behaviours
{
    #region ExpandingBehaviour (Attached DependencyProperty)
    public static readonly DependencyProperty ExpandingBehaviourProperty =
        DependencyProperty.RegisterAttached("ExpandingBehaviour", typeof(ICommand), typeof(Behaviours),
            new PropertyMetadata(OnExpandingBehaviourChanged));
    public static void SetExpandingBehaviour(DependencyObject o, ICommand value)
    {
        o.SetValue(ExpandingBehaviourProperty, value);
    }
    public static ICommand GetExpandingBehaviour(DependencyObject o)
    {
        return (ICommand) o.GetValue(ExpandingBehaviourProperty);
    }
    private static void OnExpandingBehaviourChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        TreeViewItem tvi = d as TreeViewItem;
        if (tvi != null)
        {
            ICommand ic = e.NewValue as ICommand;
            if (ic != null)
            {
                tvi.Expanded += (s, a) => 
                {
                    if (ic.CanExecute(a))
                    {
                        ic.Execute(a);

                    }
                    a.Handled = true;
                };
            }
        }
    }
    #endregion
}

Then import the name space of this class into your Xaml...

xmlns:bindTreeViewExpand="clr-namespace:BindTreeViewExpand" (your name space will be different!)

Resharper will do this for you, or give you an intellesense prompt.

Finally wire up the View Model. Use the quick and dirty method like this...

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }

Then, after the name spaces are resolved and the wiring is correct, it will start to work. Anchor your debugger in the Execute method and observe that you get a RoutedEvent argument. You can parse this to get which Tree view item was expanded.

The key aspect in this solution is the behaviour being specified in the STYLE! So it is applied to each and every TreeViewItem. No code behind either (other than the behaviour).

The behaviour I listed above marks the event as handled. You may wish to change that depending upon the behaviour you are after.

Astrict answered 27/4, 2014 at 0:32 Comment(11)
I tried it and it works pretty well. Admittedly it's quite a bit of effort for something that should be so simple, but it does the job and does it well.Wreath
Just remember not to inline the getterAstrict
@GayotFow Why in my project just call the code ` public static readonly DependencyProperty ExpandingBehaviourProperty = DependencyProperty.RegisterAttached("ExpandingBehaviour", typeof(ICommand), typeof(Behaviours), new PropertyMetadata(OnExpandingBehaviourChanged));` but never call the OnExpandingBehaviourChanged.Fluorite
@xudong125, it gets called in the InitializeComponent method when the app startsAstrict
@GayotFow Now,I found some article about [this][1] HierarchicalDataTemplates have no "Expanded" event but I still want every item in my TreeView to be able to trigger the expanded event.Fluorite
What is the meaning of the line var handler = System.Threading.Interlocked.CompareExchange(ref PropertyChanged, null, null); ?Nephogram
@HamletHakobyan, it is a safe way to get the event handler. Please see msdn.microsoft.com/en-us/library/…Astrict
Can you explain what does "safe way to get the event handler" mean? Why var handler = PropertyChanged; is not safe?Nephogram
@HamletHakobyan, the best practices model for getting the handler. You have a really good question that is not about creating an attached behaviour and should be a question in its own right. You can ask a question about why this line of code is useful and why var handler=PropertyChanged may be bad practice. Ping me if you ask such a question please.Astrict
The question is just for you and it is navigation question to push you to dig deeper and find out that the technique you are using hasn't meaning in that context.Nephogram
@HamletHakobyan, in that case thank you very much for your insight. I will dig deeper and find out the underlying issues. Thanks againAstrict

© 2022 - 2024 — McMap. All rights reserved.