【问题标题】:How to sort a treeview that has stackpanel items如何对具有堆栈面板项目的树视图进行排序
【发布时间】:2017-03-06 09:16:18
【问题描述】:

我尝试创建一些东西来快速定位和查看文件。所以我创建了一个以 StackPanels 作为项目的 TreeView。 StackPanel 包含一个图像和一个标签。

    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;
    }

可以创建逻辑文件夹(仅用于创建结构)并将文件和其他文件夹添加到文件夹(仅引用硬盘上的实际文件)。在我尝试对 TreeView 进行排序之前,这一直很好。我阅读了有关使用

对 TreeViews 进行排序的内容
    SortDescriptions.Add(new SortDescription("Header", ListSortDirection.Ascending));

显然这对我不起作用,因为我无法将“Header”与“Header.StackPanel.Label.Text”交换 当我进一步阅读时,似乎我通过不使用 MVVM (Numerically sort a List of TreeViewItems in C#) 对整个事情使用了错误的方法。

由于我几乎没有使用 MVVM 的经验,有人可以向我解释如何最好地使用 MVVM 吗?我使用“watchedFile”列表来保存文件和文件夹。

我基本上有以下文件类

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;
    }        
}

如果路径为空或为空它的文件夹。 TreeViewItem 有一个小图像,显示一个小“文件夹”或“图片”,一个显示“watchedFile.name”的标签和一个包含 watchfile.path 的不可见标签(仅显示为工具提示)。我想我应该用 DataBinding 来做这个,所以我不需要添加一个不可见的标签。

问题:

  1. 如何使用 MVVM 解决任务?
  2. 当我只有 wacthedFile.path 来区分时,如何/在哪里可以将 Image 绑定到 TreeViewItem?
  3. 如何对观看的项目进行排序?
  4. 如何跟踪 TreeView 级别(以便我可以从保存的文件重建结构)?
  5. 有没有办法在不使用 MVVM/Data-Binding 的情况下使用 StackPanel 项对 TreeView 进行排序?

非常感谢任何帮助。

【问题讨论】:

    标签: c# wpf mvvm data-binding treeview


    【解决方案1】:

    以下是如何执行这种 MVVM 方式。

    首先,编写视图模型类。在这里,我们有一个包含WatchedFile 实例集合的主视图模型,然后我们有WatchedFile 类本身。我还决定将Tag 设为一个类,而不仅仅是使用字符串。这让我们可以在 XAML 中编写数据模板,这些模板明确且专门用于显示 Tag 实例,而不是一般的字符串。 UI 充满了字符串。

    如果您没有 sn-p,则通知属性编写起来很繁琐。 I have snippets(偷他们!他们没有被钉死!)。

    一旦你有了这个,排序就没什么大不了的了。如果要对根级别项目进行排序,则为 WatchedFile

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

    但我们将在下面的 XAML 中执行此操作。

    序列化也很简单:只需让您的视图模型类可序列化即可。这里重要的是您的排序和序列化不必关心树视图项目模板中的内容。 StackPanels、GroupBoxes 等等——根本不重要,因为您的排序和序列化代码只处理您的数据类,而不是 UI 的东西。您可以从根本上更改数据模板中的视觉细节,而不必担心它会影响代码的任何其他部分。这就是 MVVM 的优点。

    视图模型:

    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
        }
    }
    

    后面的代码。请注意,我只在向导给我的内容中添加了一行。

    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();
            }
        }
    }
    

    最后是 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>
    

    XAML 不太明显。 TreeView.ResourcesTreeView 的任何子项的范围内。 HierarchicalDataTemplates 没有 x:Key 属性,这使得它们隐式。这意味着当TreeView 的项目被实例化时,每个根项目都将有一个WatchedFile 类实例用于其DataContext。由于WatchedFile 有一个隐式数据模板,它将用于填充其内容。 TreeView 是递归的,所以它使用HierarchicalDataTemplate 而不是常规的DataTemplateHierarchicalDataTemplate 添加了 ItemSource 属性,该属性告诉项目在其 DataContext 对象上查找其子项的位置。 WatchedFile.Tags 是根级树项的 ItemsSource。这些是Tag,它有自己的隐含HierarchicalDataTemplate。它没有孩子,所以它不使用HierarchicalDataTemplate.ItemsSource

    由于我们所有的集合都是ObservableCollection&lt;T&gt;,您可以随时从任何集合中添加和删除项目,并且用户界面将自动更新。您可以对WatchedFileNamePath 属性执行相同的操作,因为它实现了INotifyPropertyChanged,并且当它们的值更改时,它的属性会提高PropertyChanged。 XAML UI 在不被告知的情况下订阅所有通知事件,并且做正确的事情——假设您已经告诉它它需要知道什么才能做到这一点。

    您的代码隐藏可以使用FindResource 获取SortedWatchedFiles 并更改其SortDescriptions,但这对我来说更有意义,因为它与您如何填充树视图无关:

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

    后面的代码:

    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));
    }
    

    【讨论】:

    • 感谢您非常详细的回答 - 我非常喜欢它。到目前为止它工作得很好,它认为我对 MVVM 有了更多的了解。但是我仍然遇到了一些问题。我希望一个文件/文件夹本身有多个条目。所以我在 WatchedFile 中添加了一个可观察的集合
    • 更多问题请看下一个“答案”
    • 抱歉,我没能正确阅读;) 将 ItemsSource="{Binding Tags}" 替换为 ItemsSource="{Binding Subs}"
    • 我必须添加什么才能使 TreeView 的子项(现在也是 WatchedFiles)与根项一样被排序?
    • @Thoms 最快的方法是让WatchedFile 对集合本身进行排序。一种更受人尊敬的 MVVM 方法是使用值转换器将集合转换为已排序的 CollectionViewSource; here are two variations on that theme。多值转换器的答案(向下滚动一点)非常巧妙,因为您可以绑定到属性以控制它们在运行时的排序方式。不过我还没有测试过。
    【解决方案2】:

    否则非 MVVM 解决方案将是......

    我看到你的标题是一个有 2 个孩子的 StackPanel,你希望对标签的内容进行排序,这是第二个孩子

    您可以将标签子标签作为位置数组访问[1],因为数组是基于 0 的。

    TreeView1.Items.SortDescriptions.Clear();
    TreeView1.Items.SortDescriptions.Add(new SortDescription("Header.Children[1].Content", ListSortDirection.Ascending));
    TreeView1.Items.Refresh();
                    
    

    【讨论】:

      猜你喜欢
      • 2015-11-18
      • 2011-01-23
      • 1970-01-01
      • 2011-01-02
      • 2023-03-20
      • 2017-08-02
      • 2011-06-17
      • 2020-06-28
      • 2020-10-20
      相关资源
      最近更新 更多