views:

49

answers:

2

I'm trying to find code or a pre-packaged control that takes an object graph and displays the public properties and values of the properties (recursively) in a TreeView. Even a naive implementation is ok, I just need something to start with.

The solution must be in WPF, not winforms or com, etc...

+1  A: 

Well, this is probably a little more naive than you where hoping for, but it could possibly give you a starting point. It could do with some refactoring, but it was literally done in 15 min so take it for what it is, which is not well tested or using any WPF fancies for that matter.

First a simple UserControl which just hosts a 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>

The code behind for this will have just one property called ObjectGraph, this is set to the instance of the object you want to browse.

The tree only gets loaded with the first level of properties each node has the format PropertyName : Value or PropertyName : Type, if the property is a primitive type (see the IsPrimitive function), then the value is shown, otherwise an empty string is added as the child node. Adding the empty string indicates to the user that the node can ge expanded.

When the node is exanded a quick check is done to see if the first child is an empty string, if it is then the node is cleared and the properties for that node loaded into the tree.

So this basically builds the tree up as the node are expanded. This makes like easier for two reasons

1- No need to perform recursion

2- No need to detect cyclic references which will expand to eternity or some resource is depleted, which ever comes first.

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

Using the control is quite simple. Here I will just put the control on Form and then set the ObjectGraph to an instance of an object, I arbitrarily chose 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>

The code behind

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

Of course this would still need a lot of work, special handling for types like arrays possibly a mechanism to handle custom views to special types etc.

Chris Taylor
Great! I'll try it out.
Zachary Yates
@Zachary, just to let you know I got a few minutes so I quickly put in better handling of the dependency property.
Chris Taylor
A: 

So I took parts from Chris Taylor's example and the structure of a codeproject article and merged them into this:

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>

Wire-up code

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