How to bind an ObservableCollection to a TreeView

2019-08-02 18:24发布

问题:

I know this may be a duplicate but no solution works for me. So I have a class Technik which has these properties:

public class Technik
{
    public bool checkedTe { get; set; }

    public int TechnikID { get; set; }

    public string anlagengruppe { get; set; }

    public string techniktyp { get; set; }

    public string anlage { get; set; }

    public string bemerkung { get; set; }
}

Now I have a DataTable with 216 rows and each row is getting into a Technik object that is added into my ObservableCollection<Technik> like:

foreach (DataRow dr in dtTechnik.Rows)
{
     Technik technik = new Technik();

     technik.checkedTe = (bool)dr.ItemArray[0];
     technik.TechnikID = (int)dr.ItemArray[1];
     technik.anlagengruppe = (string)dr.ItemArray[2];
     technik.techniktyp = (string)dr.ItemArray[3];
     technik.anlage = (string)dr.ItemArray[4];
     technik.bemerkung = (string)dr.ItemArray[5];

     TechnikCollection.Add(technik);
}

I want to bind my ObservableCollection like:

* anlagengruppe
    * techniktyp
          *anlage
             * TechnikID

Right now I'm getting nowhere, so maybe you guys out there can help me. Actual my tree view looks like this:

<TreeView x:Name="treeView" HorizontalAlignment="Left" Height="850" Margin="10,0,0,0" VerticalAlignment="Top" Width="464" 
          ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Visible" ItemsSource="{Binding TechnicTable}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding TechnicTable}">
            <TextBlock  Text="{Binding Path=anlagengruppe}" />
            <HierarchicalDataTemplate.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding techniktyp}" />
                </DataTemplate>
            </HierarchicalDataTemplate.ItemTemplate>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

Edit: Maybe some of you think that my tree view ItemsSource is not the correct collection, this is the right one, there is some more code where I change collections.

回答1:

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?

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>

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> — 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.

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

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.