Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Invoke Command when TreeViewItem is Expanded

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?

like image 394
Gigi Avatar asked Apr 26 '14 21:04

Gigi


1 Answers

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.

like image 50
Gayot Fow Avatar answered Oct 14 '22 10:10

Gayot Fow