Search code examples
c#wpfmvvmdata-bindingtreeview

How to sort a treeview that has stackpanel items


i tried to create somthing to quickly locate and watch files. So i created a TreeView that has StackPanels as Items. A StackPanel contains an Image an a Label.

    private TreeViewItem createFile(string Name, string soureFile)
    {
        TreeViewItem tvi = new TreeViewItem();
        StackPanel sp = new StackPanel();
        Image i = new Image();
        Label l_Text = new Label();
        Label l_FileName = new Label();

        l_FileName.Content = soureFile;
        l_FileName.Width = 0;
        l_Text.Content = Name;

        System.Drawing.Bitmap dImg = (System.Drawing.Bitmap)Properties.Resources.ResourceManager.GetObject("Picture");
        MemoryStream ms = new MemoryStream();
        dImg.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
        BitmapImage bImg = new BitmapImage();
        bImg.BeginInit();
        bImg.StreamSource = new MemoryStream(ms.ToArray());
        bImg.EndInit();
        i.Source = bImg;
        i.Height = 20;
        i.Width = 20;

        sp.Name = "SP_File";
        sp.Orientation = Orientation.Horizontal;
        sp.Children.Add(i);
        sp.Children.Add(l_Text);
        sp.Children.Add(l_FileName);
        tvi.Header = sp;

        return tvi;
    }

One can create logical folders (just for creating a structure) and add files and other folders to the folders (just references to to the actual file on the hdd). This worked fine until i tried to sort the TreeView. I read stuff on on Sorting TreeViews with

    SortDescriptions.Add(new SortDescription("Header", ListSortDirection.Ascending));

Obviously this doesn't work for me since i cant exchange "Header" with "Header.StackPanel.Label.Text" As I read a little bit further it seems I used the wrong approach to the whole thing by not using MVVM (Numerically sort a List of TreeViewItems in C#).

Since I have litte to no experience with MVVM can someone explain to me how it is best do this with MVVM? I use a List of "watchedFile" to keep the files and folders.

I basically have the the following class for a file

class watchedFile
{
    public string name { get; private set; }
    public string path { get; private set; }
    public List<string> tags { get; private set; }

    public watchedFile(string Name, string Path, List<string> Tags)
    {
        name = Name;
        path = Path;
        tags = Tags;
    }        
}

If path is null or empty its a folder. The TreeViewItem has a little Image which shows a little "folder" or "picture", a label which shows the "watchedFile.name" and an invisible label which contains the watchedfile.path (which is only shown as a tooltip). I guess I should do this with DataBinding so I dont need to add an invisible Label.

Questions:

  1. How can I solve the task using MVVM?
  2. How/where can I bind the Image to the TreeViewItem when I have just the wacthedFile.path to distinguish?
  3. How do I sort the watched items?
  4. How do I keep track of the TreeView level (so i can rebuild the structure from a saved file)?
  5. Is there a way to sort a TreeView with StackPanel Items without using MVVM/Data-Binding?

Any help is highly appreciated.


Solution

  • Here's how to do this MVVM fashion.

    First, write viewmodel classes. Here, we've got a main viewmodel that has a collection of WatchedFile instances, and then we've got the WatchedFile class itself. I've also decided to make Tag a class, instead of just using strings. This lets us write data templates in the XAML that explicitly and exclusively will be used to display Tag instances, rather than strings in general. The UI is full of strings.

    The notification properties are tedious to write if you don't have a snippet. I have snippets (Steal them! They're not nailed down!).

    Once you've got this, sorting is no big deal. If you want to sort the root level items, those are WatchedFile.

    SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
    

    But we'll do that in XAML below.

    Serialization is simple, too: Just make your viewmodel classes serializable. The important thing here is that your sorting and serialization don't have to care what's in the treeview item templates. StackPanels, GroupBoxes, whatever -- it doesn't matter at all, because your sorting and serialization code just deals with your data classes, not the UI stuff. You can change the visual details in the data templates radically without having to worry about it affecting any other part of the code. That's what's nice about MVVM.

    Viewmodels:

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace WatchedFile.ViewModels
    {
        public class ViewModelBase : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
    
            protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    
        public class WatchedFile : ViewModelBase
        {
            #region Name Property
            private String _name = default(String);
            public String Name
            {
                get { return _name; }
                set
                {
                    if (value != _name)
                    {
                        _name = value;
                        OnPropertyChanged();
                    }
                }
            }
            #endregion Name Property
    
            #region Path Property
            private String _path = default(String);
            public String Path
            {
                get { return _path; }
                set
                {
                    if (value != _path)
                    {
                        _path = value;
                        OnPropertyChanged();
                    }
                }
            }
            #endregion Path Property
    
            #region Tags Property
            private ObservableCollection<Tag> _tags = new ObservableCollection<Tag>();
            public ObservableCollection<Tag> Tags
            {
                get { return _tags; }
                protected set
                {
                    if (value != _tags)
                    {
                        _tags = value;
                        OnPropertyChanged();
                    }
                }
            }
            #endregion Tags Property
        }
    
        public class Tag
        {
            public Tag(String value)
            {
                Value = value;
            }
            public String Value { get; private set; }
        }
    
        public class MainViewModel : ViewModelBase
        {
            public MainViewModel()
            {
                Populate();
            }
    
            public void Populate()
            {
                //  Arbitrary test info, just for display. 
                WatchedFiles = new ObservableCollection<WatchedFile>
                {
                    new WatchedFile() { Name = "foobar.txt", Path = "c:\\testfiles\\foobar.txt", Tags = { new Tag("Testfile"), new Tag("Text") } },
                    new WatchedFile() { Name = "bazfoo.txt", Path = "c:\\testfiles\\bazfoo.txt", Tags = { new Tag("Testfile"), new Tag("Text") } },
                    new WatchedFile() { Name = "whatever.xml", Path = "c:\\testfiles\\whatever.xml", Tags = { new Tag("Testfile"), new Tag("XML") } },
                };
            }
    
            #region WatchedFiles Property
            private ObservableCollection<WatchedFile> _watchedFiles = new ObservableCollection<WatchedFile>();
            public ObservableCollection<WatchedFile> WatchedFiles
            {
                get { return _watchedFiles; }
                protected set
                {
                    if (value != _watchedFiles)
                    {
                        _watchedFiles = value;
                        OnPropertyChanged();
                    }
                }
            }
            #endregion WatchedFiles Property
        }
    }
    

    Code behind. Note I only added one line here to what the wizard gave me.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    namespace WatchedFile
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                DataContext = new ViewModels.MainViewModel();
            }
        }
    }
    

    And lastly the XAML:

    <Window 
        x:Class="WatchedFile.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:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
        xmlns:local="clr-namespace:WatchedFile"
        xmlns:vm="clr-namespace:WatchedFile.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
        <Window.Resources>
            <CollectionViewSource 
                    x:Key="SortedWatchedFiles" 
                    Source="{Binding WatchedFiles}">
                <CollectionViewSource.SortDescriptions>
                    <scm:SortDescription PropertyName="Name" Direction="Ascending" />
                </CollectionViewSource.SortDescriptions>
            </CollectionViewSource>
        </Window.Resources>
        <Grid>
            <TreeView
                ItemsSource="{Binding Source={StaticResource SortedWatchedFiles}}"
                >
                <TreeView.Resources>
                    <HierarchicalDataTemplate 
                        DataType="{x:Type vm:WatchedFile}"
                        ItemsSource="{Binding Tags}"
                        >
                        <TextBlock 
                            Text="{Binding Name}" 
                            ToolTip="{Binding Path}"
                            />
                    </HierarchicalDataTemplate>
                    <HierarchicalDataTemplate 
                        DataType="{x:Type vm:Tag}"
                        >
                        <TextBlock 
                            Text="{Binding Value}" 
                            />
                    </HierarchicalDataTemplate>
                </TreeView.Resources>
            </TreeView>
        </Grid>
    </Window>
    

    The XAML is less than obvious. TreeView.Resources is in scope for any child of the TreeView. The HierarchicalDataTemplates don't have an x:Key property, which makes them implicit. That means that when the TreeView's items are instantiated, each of the root items will have a WatchedFile class instance for its DataContext. Since WatchedFile has an implicit data template, that will be used to fill in its content. TreeView is recursive, so it uses HierarchicalDataTemplate instead of regular DataTemplate. HierarchicalDataTemplate adds the ItemSource property, which tells the item where to look for its children on its DataContext object. WatchedFile.Tags is the ItemsSource for the root-level tree items. Those are Tag, which has its own implicit HierarchicalDataTemplate. It doesn't have children so it doesn't use HierarchicalDataTemplate.ItemsSource.

    Since all our collections are ObservableCollection<T>, you can add and remove items from any collection at any time and the UI will update automagically. You can do the same with the Name and Path properties of WatchedFile, because it implements INotifyPropertyChanged and its properties raise PropertyChanged when their values change. The XAML UI subscribes to all the notification events without being told, and does the right thing -- assuming you've told it what it needs to know to do that.

    Your codebehind can grab SortedWatchedFiles with FindResource and change its SortDescriptions, but this makes more sense to me, since it's agnostic about how you're populating the treeview:

        <Button Content="Re-Sort" Click="Button_Click" HorizontalAlignment="Left" />
    
        <!-- ... snip ... -->
    
        <TreeView
            x:Name="WatchedFilesTreeView"
            ...etc. as before...
    

    Code behind:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        var cv = CollectionViewSource.GetDefaultView(WatchedFilesTreeView.ItemsSource);
    
        cv.SortDescriptions.Clear();
        cv.SortDescriptions.Add(
            new System.ComponentModel.SortDescription("Name", 
                System.ComponentModel.ListSortDirection.Descending));
    }