Поиск элемента управления представлением дерева объектов для WPF

Я пытаюсь найти код или предварительно упакованный элемент управления, который принимает граф объектов и отображает общедоступные свойства и значения свойств (рекурсивно) в TreeView. Даже наивная реализация в порядке, мне просто нужно что-то начать.

Решение должно быть в WPF, а не winforms или com и т.д.

Ответ 1

Итак, я взял части из примера Криса Тейлора и структуру статьи кодекса и объединил их в это:

TreeView xaml:

<TreeView Name="tvObjectGraph" ItemsSource="{Binding FirstGeneration}" Margin="12,41,12,12" FontSize="13" FontFamily="Consolas">
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
            <Setter Property="FontWeight" Value="Normal" />
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="FontWeight" Value="Bold" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TreeView.ItemContainerStyle>
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock Text="{Binding Name}" Grid.Column="0" Grid.Row="0" Padding="2,0" />
                <TextBlock Text="{Binding Type}" Grid.Column="1" Grid.Row="0" Padding="2,0" />
                <TextBlock Text="{Binding Value}" Grid.Column="2" Grid.Row="0" Padding="2,0" />
            </Grid>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

Код подключения

void DisplayObjectGraph(object graph)
{
    var hierarchy = new ObjectViewModelHierarchy(graph);
    tvObjectGraph.DataContext = hierarchy;
}

ObjectViewModel.cs:

public class ObjectViewModel : INotifyPropertyChanged
{
    ReadOnlyCollection<ObjectViewModel> _children;
    readonly ObjectViewModel _parent;
    readonly object _object;
    readonly PropertyInfo _info;
    readonly Type _type;

    bool _isExpanded;
    bool _isSelected;

    public ObjectViewModel(object obj)
        : this(obj, null, null)
    {
    }

    ObjectViewModel(object obj, PropertyInfo info, ObjectViewModel parent)
    {
        _object = obj;
        _info = info;
        if (_object != null)
        {
            _type = obj.GetType();
            if (!IsPrintableType(_type))
            {
                // load the _children object with an empty collection to allow the + expander to be shown
                _children = new ReadOnlyCollection<ObjectViewModel>(new ObjectViewModel[] { new ObjectViewModel(null) });
            }
        }
        _parent = parent;
    }

    public void LoadChildren()
    {
        if (_object != null)
        {
            // exclude value types and strings from listing child members
            if (!IsPrintableType(_type))
            {
                // the public properties of this object are its children
                var children = _type.GetProperties()
                    .Where(p => !p.GetIndexParameters().Any()) // exclude indexed parameters for now
                    .Select(p => new ObjectViewModel(p.GetValue(_object, null), p, this))
                    .ToList();

                // if this is a collection type, add the contained items to the children
                var collection = _object as IEnumerable;
                if (collection != null)
                {
                    foreach (var item in collection)
                    {
                        children.Add(new ObjectViewModel(item, null, this)); // todo: add something to view the index value
                    }
                }

                _children = new ReadOnlyCollection<ObjectViewModel>(children);
                this.OnPropertyChanged("Children");
            }
        }
    }

    /// <summary>
    /// Gets a value indicating if the object graph can display this type without enumerating its children
    /// </summary>
    static bool IsPrintableType(Type type)
    {
        return type != null && (
            type.IsPrimitive ||
            type.IsAssignableFrom(typeof(string)) ||
            type.IsEnum);
    }

    public ObjectViewModel Parent
    {
        get { return _parent; }
    }

    public PropertyInfo Info
    {
        get { return _info; }
    }

    public ReadOnlyCollection<ObjectViewModel> Children
    {
        get { return _children; }
    }

    public string Type
    {
        get
        {
            var type = string.Empty;
            if (_object != null)
            {
                type = string.Format("({0})", _type.Name);
            }
            else
            {
                if (_info != null)
                {
                    type = string.Format("({0})", _info.PropertyType.Name);
                }
            }
            return type;
        }
    }

    public string Name
    {
        get
        {
            var name = string.Empty;
            if (_info != null)
            {
                name = _info.Name;
            }
            return name;
        }
    }

    public string Value
    {
        get
        {
            var value = string.Empty;
            if (_object != null)
            {
                if (IsPrintableType(_type))
                {
                    value = _object.ToString();
                }
            }
            else
            {
                value = "<null>";
            }
            return value;
        }
    }

    #region Presentation Members

    public bool IsExpanded
    {
        get { return _isExpanded; }
        set
        {
            if (_isExpanded != value)
            {
                _isExpanded = value;
                if (_isExpanded)
                {
                    LoadChildren();
                }
                this.OnPropertyChanged("IsExpanded");
            }

            // Expand all the way up to the root.
            if (_isExpanded && _parent != null)
            {
                _parent.IsExpanded = true;
            }
        }
    }

    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (_isSelected != value)
            {
                _isSelected = value;
                this.OnPropertyChanged("IsSelected");
            }
        }
    }

    public bool NameContains(string text)
    {
        if (String.IsNullOrEmpty(text) || String.IsNullOrEmpty(Name))
        {
            return false;
        }

        return Name.IndexOf(text, StringComparison.InvariantCultureIgnoreCase) > -1;
    }

    public bool ValueContains(string text)
    {
        if (String.IsNullOrEmpty(text) || String.IsNullOrEmpty(Value))
        {
            return false;
        }

        return Value.IndexOf(text, StringComparison.InvariantCultureIgnoreCase) > -1;
    }

    #endregion

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion
}

ObjectViewModelHierarchy.cs:

public class ObjectViewModelHierarchy
{
    readonly ReadOnlyCollection<ObjectViewModel> _firstGeneration;
    readonly ObjectViewModel _rootObject;

    public ObjectViewModelHierarchy(object rootObject)
    {
        _rootObject = new ObjectViewModel(rootObject);
        _firstGeneration = new ReadOnlyCollection<ObjectViewModel>(new ObjectViewModel[] { _rootObject });
    }

    public ReadOnlyCollection<ObjectViewModel> FirstGeneration
    {
        get { return _firstGeneration; }
    }
}

Ответ 2

Ну, это, наверное, немного наивнее, чем вы, где надеялись, но это могло бы дать вам отправную точку. Это может быть связано с некоторыми рефакторингами, но это было сделано буквально за 15 минут, поэтому возьмите его за то, что оно есть, которое не хорошо протестировано или использует какие-либо фантазии WPF в этом отношении.

Сначала простой UserControl, который просто содержит TreeView

<UserControl x:Class="ObjectBrowser.PropertyTree"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
    <TreeView Name="treeView1" TreeViewItem.Expanded="treeView1_Expanded" />
  </Grid>
</UserControl>

В коде для этого будет только одно свойство с именем ObjectGraph, это устанавливается в экземпляр объекта, который вы хотите просмотреть.

Дерево загружается только с первым уровнем свойств, каждый из которых node имеет формат PropertyName: Value или PropertyName: Type, если свойство является примитивным типом (см. функцию IsPrimitive), тогда отображается значение, иначе пустая строка добавляется как дочерний элемент node. Добавление пустой строки указывает пользователю, что расширение node может быть расширено.

В случае использования node выполняется быстрая проверка, чтобы убедиться, что первый ребенок является пустой строкой, если он затем очищен node, а свойства для этого node загружены в дерево.

Итак, это в основном создает дерево вверх по мере расширения node. Это упрощается по двум причинам.

1 - Не нужно выполнять рекурсию

2 - Не нужно обнаруживать циклические ссылки, которые будут расширяться до вечности, или какой-то ресурс исчерпан, что когда-либо наступит раньше.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Reflection;

namespace ObjectBrowser
{
  public partial class PropertyTree : UserControl
  {
    public PropertyTree()
    {
      InitializeComponent();
    }

    private void treeView1_Expanded(object sender, RoutedEventArgs e)
    {
      TreeViewItem item = e.OriginalSource as TreeViewItem;
      if (item.Items.Count == 1 && item.Items[0].ToString() == string.Empty)
      {
        LoadGraph(item.Items, item.Tag);
      }
    }

    public object ObjectGraph
    {
      get { return (object)GetValue(ObjectGraphProperty); }
      set { SetValue(ObjectGraphProperty, value); }
    }

    public static readonly DependencyProperty ObjectGraphProperty =
        DependencyProperty.Register("ObjectGraph", typeof(object), typeof(PropertyTree),
        new UIPropertyMetadata(0, OnObjectGraphPropertyChanged));

    private static void OnObjectGraphPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
      PropertyTree control = source as PropertyTree;
      if (control != null)
      {
        control.OnObjectGraphChanged(source, EventArgs.Empty);
      }
    }

    protected virtual void OnObjectGraphChanged(object sender, EventArgs e)
    {
      LoadGraph(treeView1.Items, ObjectGraph);
    }

    private void LoadGraph(ItemCollection nodeItems, object instance)
    {
      nodeItems.Clear();
      if (instance == null) return;      
      Type instanceType = instance.GetType();      
      foreach (PropertyInfo pi in instanceType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
      {                
        object propertyValue =pi.GetValue(instance, null);
        TreeViewItem item = new TreeViewItem();
        item.Header = BuildItemText(instance, pi, propertyValue);
        if (!IsPrimitive(pi) && propertyValue != null)
        {
          item.Items.Add(string.Empty);
          item.Tag = propertyValue;
        }

        nodeItems.Add(item);
      }
    }

    private string BuildItemText(object instance, PropertyInfo pi, object value)
    {
      string s = string.Empty;
      if (value == null)
      {
        s = "<null>";
      }
      else if (IsPrimitive(pi))
      {
        s = value.ToString();
      }
      else
      {
        s = pi.PropertyType.Name;
      }
      return pi.Name + " : " + s;
    }

    private bool IsPrimitive(PropertyInfo pi)
    {
      return pi.PropertyType.IsPrimitive || typeof(string) == pi.PropertyType;
    }       
  }
}

Использование элемента управления довольно простое. Здесь я просто поставлю элемент управления в Form и затем установлю ObjectGraph на экземпляр объекта, я произвольно выбрал XmlDataProvider.

XAML

<Window x:Class="ObjectBrowser.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:ObjectBrowser" Loaded="Window_Loaded">
    <Grid>
    <my:PropertyTree x:Name="propertyTree1" />
  </Grid>
</Window>

Код

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace ObjectBrowser
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
      var o = new XmlDataProvider();
      o.Source = new Uri("http://www.stackoverflow.com");
      propertyTree1.ObjectGraph = o;
    }
  }
}

Конечно, это все равно потребует большой работы, особая обработка для таких типов, как массивы, возможно, механизм обработки пользовательских представлений специальным типам и т.д.