0
votes

I am trying to get the selected item from my TreeView but having some problem.

I am following MVVM archetecture. My ViewModel contains a collection of a class which is in my Model. So I have binded the ItemSource of TreeView with that collection. I want to bind the selectedItem of my TreeView to an item of the binded collection. How do I do that? Here is the code for SelectedItem and IsSelected Property.

    private static sourceData _selectedItem = null;
    /// <summary>
    /// Selected Item in the tree
    /// </summary>
    public static sourceData SelectedItem
    {
        get { return _selectedItem; }
        set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
            }
        }
    }

    private bool _isSelected;
    /// <summary>
    /// Get/Set for Selected node
    /// </summary>
    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (_isSelected != value)
            {
                _isSelected = value;

                if (_isSelected)
                {
                    SelectedItem = this;
                    OnPropertyChanged("IsSelected");
                }
            }
        }
    }

    /// <summary>
    /// Property changed event
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Property changed event handler
    /// </summary>
    /// <param name="propertyName"></param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }

When I debug this, int SelectedItem = this; 'this' pointer contains the collection to which my treeview is binded. I needed to have a SelectedDataSource so that I could assign it to the selected Item. How can I make my TreeView return me the selectedItem in the collection??

FYi, this is my XAML code for TreeView

<TreeView Margin="5,0,0,0" ItemsSource="{Binding SourceData}"  Width="390">
                    <TreeView.ItemContainerStyle>
                        <Style TargetType="{x:Type TreeViewItem}">
                            <Setter Property="IsSelected" Value="{Binding DataContext.IsSelected, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
                          <Setter Property="ContextMenu">
                                <Setter.Value>
                                    <ContextMenu Name="contextMenu" DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}" >
                                        <MenuItem Name="menuItem" Header="Rename" Command="{Binding RenameCommand}" />
                                    </ContextMenu>
                                </Setter.Value>
                            </Setter>
                            <Style.Triggers>
                                <Trigger Property="IsSelected" Value="True">
                                </Trigger>
                            </Style.Triggers>
                        </Style>
                    </TreeView.ItemContainerStyle>

PS: If I write the above code in my Model, I get everything working perfectly fine. But I can not write the above code in Model, it has to be in VM.

1
I have been using an Attached Behaviour to bind to an ICommand in my VM. It's clean, but you have to remember that the TV SelectedItem is a readonly property, so the VM can only inspect it.Gayot Fow
I only want to read the selectedItem. I dont need to set it. So how do you use Attached behavior to bind to an ICommmand in VM?WAQ
I put in an answer. It gives you the whole shebang, but you will probably have to tweak it to fit your own needs...Gayot Fow

1 Answers

0
votes

Further to the commentary on your question, the WPF TreeView gives some unique challenges to the MVVM developer, and among these is detecting the currently selected item. For this you can use attached behaviour. To begin, write a static class to contain the behaviour...

   public static class TvBehaviour
    {
        #region TvSelectedItemChangedBehaviour (Attached DependencyProperty)
        public static readonly DependencyProperty TvSelectedItemChangedBehaviourProperty =
            DependencyProperty.RegisterAttached("TvSelectedItemChangedBehaviour",
                                                typeof (ICommand),
                                                typeof (TvBehaviour),
                                                new PropertyMetadata(
                                                    OnTvSelectedItemChangedBehaviourChanged));

        public static void SetTvSelectedItemChangedBehaviour(DependencyObject o, ICommand value)
        {
            o.SetValue(TvSelectedItemChangedBehaviourProperty, value);
        }
        public static ICommand GetTvSelectedItemChangedBehaviour(DependencyObject o)
        {
            return (ICommand) o.GetValue(TvSelectedItemChangedBehaviourProperty);
        }
        private static void OnTvSelectedItemChangedBehaviourChanged(DependencyObject d,
                                                                    DependencyPropertyChangedEventArgs e)
        {
            TreeView tv = d as TreeView;
            if (tv != null)
            {
                tv.SelectedItemChanged += (s, a) =>
                    {
                        GetTvSelectedItemChangedBehaviour(tv).Execute(a.NewValue);
                        a.Handled = true;
                    };
            }
        }
        #endregion

}

Then import the class's namespace into your Xaml (using xmlns). You can then declare a TreeView along these lines...

    <TreeView ItemsSource="{Binding MyList}" 
              ItemTemplate="{StaticResource My_data_template}"
              tvBinding:TvBehaviour.TvSelectedItemChangedBehaviour="{Binding             
                           SelectedItemCommand}"
              SelectedValuePath="Name"
              >
    </TreeView>

This 'wires' the TV behaviour to an ICommand in your VM. Finally, declare the ICommand in your VM...

public ICommand SelectedItemCommand { get; set; }

And initialize it...

 SelectedItemCommand = new RelayCommand(ExecuteSelectedItemCommand, 
                                         CanExecuteSelectedItemCommand);

And then implement your delegates...

    private void ExecuteSelectedItemCommand(object obj)
    {
        // downcast 'obj' to get the instance of the selected item
    }
    private bool CanExecuteSelectedItemCommand(object obj)
    {
        return true;
    }

When the user selects a TV item, your 'execute' delegate will get a boxed instance of the item, and you can unbox it and etc etc etc.

Note that the attached behaviour in this example assumes that the TV's lifetime is the same as the app, otherwise you have to unwire the attached behaviour. It also assumes that the TV ItemsSource is binding to something sensible.

That will solve the problem of getting the TV SelectedItem while remaining MVVM compliant (if such a thing as MVVM compliant exists).

The Relay Command class I used was taken from the linked article in MSDN. For reference purposes, here it is...

public class RelayCommand : ICommand
{   //http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }
    public bool CanExecute(object parameter)
    {
        return _canExecute(parameter);
    }
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
    public void Execute(object parameter)
    {
        _execute(parameter);
    }
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;
}

Use the MSDN link above to get more info about this class.