Search code examples
c#wpfeventstreeviewitem

How to set event for every TreeViewItem?


I want to set an event for every TreeViewItem i'm creating dynamically in code. I'm using the following XAML code:

<Window.Resources>
        <Style TargetType="TreeViewItem">
            <EventSetter Event="Selected" Handler="TreeViewItemSelectionEvent"/>
            <EventSetter Event="MouseRightButtonDown" Handler="TreeViewItemRightClickEvent"/>
        </Style>
</Window.Resources>

But this only works on a root TreeViewItem in the TreeView. If i click on another item, which is a child of the root, then i allways get back the root.

Can i make this work somehow? I'd love this method, thus you don't need to handle the events in your code which makes it look cleaner.


Solution

  • TL;DR: Use HierarchicalDataTemplate and Styles to display data in a TreeView. Dynamically loading data in your viewmodel will automatically update the TreeView.

    Do not manually create TreeViewItems.

    In MVVM, what matters the most is the data and the architecture of your data. The general pattern is to define your data and separately define how the view will display your data. Your view is dependent on your data, not the other way around.

    So let's create a ProductViewModel which has a ProductName, a list of sub products and a IsSelected property. We equip this class with a method LoadSubProductsCollectionFromDataSource which retrieves data from whatever data source you may have. Here, I just load somme dummy items.

    public class ProductViewModel {
        /// <summary>
        /// Backing field for the IsSelected property
        /// </summary>
        private bool _isSelected;
    
        /// <summary>
        /// Gets or sets the collection of materials used to build this Product.
        /// </summary>
        public ObservableCollection<ProductViewModel> SubProducts { get; set; } = new ObservableCollection<ProductViewModel>();
    
        /// <summary>
        /// Gets or sets the name of this product.
        /// </summary>
        public string ProductName { get; set; }
    
        /// <summary>
        /// Gets or sets the selected state of this product.
        /// </summary>
        public bool IsSelected {
            get => _isSelected;
            set {
                //The product has been selected or deselected.
                if (!_isSelected && SubProducts.Count == 0) {
                    //We load data into it if not already done.
                    LoadSubProductsCollectionFromDataSource();
                }
                _isSelected = value;
            }
        }
    
        /// <summary>
        /// Loads sub products data into this product.
        /// </summary>
        private void LoadSubProductsCollectionFromDataSource() {
            //..
            //Do stuff to retrieve your data dynamically and
            //add them to the SubProducts collection.
            //...
            for (int i = 0; i < 5; i++) {
                //Add dummy items
                SubProducts.Add(new ProductViewModel() { ProductName = "Some product " + i.ToString() });
            }
        }
    }
    
    

    In your MainWindow.xaml.cs, initialize and expose a collection of view model objects like this:

    public partial class MainWindow : Window {
        /// <summary>
        /// Exposes the root product of the tree
        /// </summary>
        public ObservableCollection<ProductViewModel> RootProducts { get; } = new ObservableCollection<ProductViewModel>();
    
        public MainWindow() {
            InitializeComponent();
            RootProducts.Add(new ProductViewModel() { ProductName = "Root product" });
        }
    }
    

    This collection would normally be stored in a main viewmodel object but for simplicity I just created it in the MainWindow. Notice how I expose it as a property (to allow Binding) and as an ObservableCollection (to automatically notify the view when the collection changes).

    Finally, tell your view how to display your ProductViewModel objects using a TreeView:

    <Window x:Class="WpfApp1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            mc:Ignorable="d"
            x:Name="window"
            Title="MainWindow" Height="450" Width="800">
        <Window.Resources>
            <!--Tell the treeview how to hierarchically display and organize ProductViewModel items-->
            <HierarchicalDataTemplate DataType="{x:Type local:ProductViewModel}" ItemsSource="{Binding SubProducts}">
                <TextBlock Text="{Binding ProductName}"></TextBlock>
            </HierarchicalDataTemplate>
            <!--Tell each treeviewitem to bind IsSelected to the PoductViewModel.ISSelected property.-->
            <Style TargetType="TreeViewItem">
                <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"></Setter>
            </Style>
        </Window.Resources>
        <Grid>
            <TreeView ItemsSource="{Binding ElementName=window, Path=RootProducts}"/>
        </Grid>
    </Window>
    

    Now, every time you select a TreeViewItem in your TreeView, its IsSelected property is set to true (this is the behavior of a TreeViewItem). Because of our binding, it also sets to true the IsSelected property of the corresponding ProductViewModel. In the setter of this property, we make a call to populate the list of subproducts. Because this list is actually an ObservableCollection, it notifies the View (the TreeView) which knows it should update itself with new TreeViewItems.