且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

如何以分层方式将ObservableCollection绑定到TreeView?

更新时间:2022-06-11 03:17:16

我对此设计表示怀疑.为什么您认为呈现单个对象及其属性值对用户很有帮助,就像该对象具有某种层次结构一样?

I am skeptical of this design. Why do you feel it's useful and helpful to the user to present a single object and its property values as if that object had some hierarchical structure to it?

如果您要做的只是在用户界面上添加一些 visual 结构,则无需使用TreeView即可轻松完成.例如:

If all you're trying to do is impose some visual structure on the user interface, that's easily done without using TreeView. For example:

class TableItem
{
    public string Property1 { get; set; }
    public string Property2 { get; set; }
    public string Property3 { get; set; }

    public TableItem() { }

    public TableItem(string property1, string property2, string property3)
    {
        Property1 = property1;
        Property2 = property2;
        Property3 = property3;
    }
}

class ViewModel
{
    public ObservableCollection<TableItem> TableItems { get; } = new ObservableCollection<TableItem>();
}

<Window x:Class="TestSO46300831HiearchicalObservable.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:l="clr-namespace:TestSO46300831HiearchicalObservable"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <l:ViewModel>
      <l:ViewModel.TableItems>
        <l:TableItem Property1="Item #1, property #1"
                     Property2="Item #1, property #2"
                     Property3="Item #1, property #3"/>
        <l:TableItem Property1="Item #2, property #1"
                     Property2="Item #2, property #2"
                     Property3="Item #2, property #3"/>
        <l:TableItem Property1="Item #3, property #1"
                     Property2="Item #3, property #2"
                     Property3="Item #3, property #3"/>
      </l:ViewModel.TableItems>
    </l:ViewModel>
  </Window.DataContext>

  <Window.Resources>
    <DataTemplate DataType="{x:Type l:TableItem}">
      <StackPanel>
        <TextBlock Text="{Binding Property1}"/>
        <TextBlock Text="{Binding Property1}" Margin="10,0,0,0"/>
        <TextBlock Text="{Binding Property1}" Margin="20,0,0,0"/>
      </StackPanel>
    </DataTemplate>
  </Window.Resources>

  <StackPanel>
    <ListBox ItemsSource="{Binding TableItems}"/>
  </StackPanel>
</Window>

也就是说,如果您必须使用TreeView,并且希望在修改集合时更新视图,那么在我看来,您可以通过使用实现INotifyCollectionChanged(只需继承ObservableCollection<T>并跟踪原始集合即可轻松完成.需要中间集合,以便可以将项目从原始单对象项目转换为可以与TreeView一起使用的分层项目类型类.例如:

That said, if you must use TreeView and you want for the view to update as the collection is modified, it seems to me you can accomplish that by using an intermediate collection that implements INotifyCollectionChanged (easily done simply by inheriting ObservableCollection<T> and tracking the original collection. The intermediate collection is needed, so that items can be converted from the original single-object item to a hierarchical item type that can be used with the TreeView class. For example:

class HierarchicalTableItem
{
    public string Text { get; }
    public IReadOnlyList<HierarchicalTableItem> Items { get; }

    public HierarchicalTableItem(string text, HierarchicalTableItem child = null)
    {
        Text = text;
        Items = child != null ? new[] { child } : null;
    }
}

class ViewModel
{
    public ICommand AddCommand { get; }
    public ICommand InsertCommand { get; }
    public ICommand RemoveCommand { get; }

    public int Index { get; set; }

    public ObservableCollection<TableItem> TableItems { get; } = new ObservableCollection<TableItem>();

    public ViewModel()
    {
        AddCommand = new DelegateCommand(() => TableItems.Add(_CreateTableItem()));
        InsertCommand = new DelegateCommand(() => TableItems.Insert(Index, _CreateTableItem()));
        RemoveCommand = new DelegateCommand(() => TableItems.RemoveAt(Index));
    }

    private int _itemNumber;

    private TableItem _CreateTableItem()
    {
        _itemNumber = (_itemNumber < TableItems.Count ? TableItems.Count : _itemNumber) + 1;

        return new TableItem(
            $"Item #{_itemNumber}, property #1",
            $"Item #{_itemNumber}, property #2",
            $"Item #{_itemNumber}, property #3");
    }
}

class ConvertingObservableCollection<T> : ObservableCollection<object>
{
    private readonly IValueConverter _converter;
    private readonly ObservableCollection<T> _collection;

    public ConvertingObservableCollection(IValueConverter converter, ObservableCollection<T> collection)
    {
        _converter = converter;
        _collection = collection;
        _ResetItems();
        _collection.CollectionChanged += _OnCollectionChanged;
    }

    private void _OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                _AddItems(e);
                break;
            case NotifyCollectionChangedAction.Move:
                _RemoveItems(e);
                _AddItems(e);
                break;
            case NotifyCollectionChangedAction.Remove:
                _RemoveItems(e);
                break;
            case NotifyCollectionChangedAction.Replace:
                _ReplaceItems(e);
                break;
            case NotifyCollectionChangedAction.Reset:
                _ResetItems();
                break;
        }
    }

    private void _ReplaceItems(NotifyCollectionChangedEventArgs e)
    {
        for (int i = 0; i < e.NewItems.Count; i++)
        {
            this[i] = _Convert(e.NewItems[i]);
        }
    }

    private void _AddItems(NotifyCollectionChangedEventArgs e)
    {
        for (int i = 0; i < e.NewItems.Count; i++)
        {
            Insert(i + e.NewStartingIndex, _Convert(e.NewItems[i]));
        }
    }

    private void _RemoveItems(NotifyCollectionChangedEventArgs e)
    {
        for (int i = e.OldItems.Count - 1; i >= 0; i--)
        {
            RemoveAt(i + e.OldStartingIndex);
        }
    }

    private void _ResetItems()
    {
        Clear();
        foreach (T t in _collection)
        {
            Add(_Convert(t));
        }
    }

    private object _Convert(object value)
    {
        return _converter.Convert(value, typeof(T), null, null);
    }
}

class TableItemHierarchicalConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        TableItem tableItem = value as TableItem;

        if (tableItem == null)
        {
            return Binding.DoNothing;
        }

        return new HierarchicalTableItem(tableItem.Property1,
                    new HierarchicalTableItem(tableItem.Property2,
                        new HierarchicalTableItem(tableItem.Property3)));
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

class ConvertingCollectionConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        IValueConverter converter = parameter as IValueConverter;

        if (converter == null || value == null ||
            value.GetType().GetGenericTypeDefinition() != typeof(ObservableCollection<>))
        {
            return Binding.DoNothing;
        }

        Type resultType = typeof(ConvertingObservableCollection<>).MakeGenericType(value.GetType().GenericTypeArguments);

        return Activator.CreateInstance(resultType, converter, value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

<Window x:Class="TestSO46300831HiearchicalObservable.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:l="clr-namespace:TestSO46300831HiearchicalObservable"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <l:ViewModel>
      <l:ViewModel.TableItems>
        <l:TableItem Property1="Item #1, property #1"
                     Property2="Item #1, property #2"
                     Property3="Item #1, property #3"/>
        <l:TableItem Property1="Item #2, property #1"
                     Property2="Item #2, property #2"
                     Property3="Item #2, property #3"/>
        <l:TableItem Property1="Item #3, property #1"
                     Property2="Item #3, property #2"
                     Property3="Item #3, property #3"/>
      </l:ViewModel.TableItems>
    </l:ViewModel>
  </Window.DataContext>

  <Window.Resources>
    <l:ConvertingCollectionConverter x:Key="convertingCollectionConverter1"/>
    <l:TableItemHierarchicalConverter x:Key="tableItemConverter1"/>
  </Window.Resources>

  <ScrollViewer>
    <StackPanel>
      <UniformGrid Columns="4">
        <Button Content="Add" Command="{Binding AddCommand}"/>
        <Button Content="Insert" Command="{Binding InsertCommand}"/>
        <Button Content="Remove" Command="{Binding RemoveCommand}"/>
        <TextBox Text="{Binding Index}"/>
      </UniformGrid>
      <TreeView ItemsSource="{Binding TableItems,
              Converter={StaticResource convertingCollectionConverter1},
              ConverterParameter={StaticResource tableItemConverter1}}">
        <TreeView.ItemTemplate>
          <HierarchicalDataTemplate ItemsSource="{Binding Items}">
            <TextBlock Text="{Binding Text}"/>
          </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
      </TreeView>
    </StackPanel>
  </ScrollViewer>
</Window>

此替代方法依赖于三个关键类:

This alternative relies on three key classes:

  • ConvertingObservableCollection<T>—这项工作是监视原始集合并根据该原始集合的当前状态显示转换后的项目.
  • ConvertingCollectionConverter—出于绑定到TreeView的目的,这会将原始集合转换为ConvertingObservableCollection<T>对象.
  • TableItemHierarchicalConverter—这样会将各个原始项目对象转换为适合在TreeView中显示的对象层次.
  • ConvertingObservableCollection<T> — This does the work of watching the original collection and presenting converted items according to the current state of that original collection.
  • ConvertingCollectionConverter — This converts the original collection to the ConvertingObservableCollection<T> object for the purpose of binding to the TreeView.
  • TableItemHierarchicalConverter — This converts the individual original item objects into a hierarchy of objects suitable for display in the TreeView.

当然,还有简单的容器类HierarchicalTableItem,用于表示每个表项的层次结构.

Of course, there is also the simple container class HierarchicalTableItem which is used to represent the hierarchy for each table item.

最后,要记住的关键是,对于TreeView,您必须呈现具有递归性质的项目,以便可以使用单个HierarchicalDataTemplate元素来定义如何当前树的每个级别.这意味着单个数据项必须具有可用于模板ItemsSource的某些属性,该属性本身就是同一类型的数据项的某种类型的集合.

Ultimately, the key to remember is that for a TreeView, you must be presenting items that have a recursive nature, such that a single HierarchicalDataTemplate element can be used to define how to present each level of the tree. This means that a single data item must have some property that can be used for the ItemsSource of the template, which is itself some type of collection of the same type of data item.