« When to switch to WPF? | Main | Ouch! That hurt! »

WPF Object Browser Demonstration

With no particular goal, except to write some interesting WPF code, I've created the "Wired Prairie Object Browser." The project, while somewhat useful in its current state, is primarily designed to showcase WPF.

You can see an enlarged screen shot by clicking on most of the images.

image

If you're familiar with the Macintosh OS X Finder view, you'll see the similarity between the user interface of this application. The core aspect of this user experience is a horizontal scrolling area that expands to the right with the children of the currently selected item to the left.

Best explained by a walk-through.

The application starts like this:

image

I clicked on the first node:

image

And so on:

image image

Clicking around changes the various lists:

image

Support for individual per-column filtering is included:

image

image

The application also supports dragging new assemblies onto the application -- which adds them to the list of objects and namespaces. The application always defaults to opening mscorlib.dll by default (home of System.String). (To add a new assembly, just drag the file name from an Explorer window onto this application).

Private and protected members are highlighted with different images:

image

The application makes heavy use of DataTemplates in WPF to do it's magic.

For example, there are 3 types of lists in the application: the horizontal list which holds the various namespaces and classes lists; the list of namespaces and classes; and the list of properties and methods and events.

<DataTemplate
    DataType="{x:Type local:ContainerNodeCollection}">
    <Border
        BorderThickness="6"
        BorderBrush="Silver">
        <DockPanel
            LastChildFill="True">
            <TextBox
                Name="FilterTextBox"
                TextChanged="FilterTextBox_TextChanged"
                DockPanel.Dock="Top"
                Margin="2,2,2,2"
                Style="{DynamicResource FilterTextBoxStyle}"></TextBox>
            <ListBox
                Name="CollectionListBox"
                ItemsSource="{Binding Mode=OneTime}"
                SelectionChanged="ListBox_SelectionChanged"
                Loaded="ListBox_Loaded"
                MinWidth="300"
                MaxWidth="300"
                MinHeight="30" />
        </DockPanel>
    </Border>
</DataTemplate>

Above is the DataTemplate for the list which shows namespaces and classes.

Here's a snippet from the template that shows information about the specific details of a class:

<DataTemplate
    DataType="{x:Type local:ComboCollection}">
    <DataTemplate.Resources>
        <!-- these are privately used within the combo collection -->
        <DataTemplate
            DataType="{x:Type sysreflect:PropertyInfo}">
            <DockPanel
                LastChildFill="True">
                <Canvas
                    Height="16"
                    Width="16">
                    <Image
                        Height="16"

This one is particularly interesting from a WPF perspective as it's redefining a resource locally within the DataTemplate. Depending on the use of the PropertyInfo object, it displays very differently.When it's shown in this list, it's just a small icon, with a title. When it's shown as detail of a property, it shows with more detail:

image

On the left is the template contained as a resource within a DataTemplate (the example snippet above), and the right is the other. I could have used a complex set of DataTriggers, but this seemed like the simpler, more maintainable solution rather than relying on a single DataTemplate that had to have knowledge of all the places it could be used.

The code treats the protected or private state of a method as a sneaky image overlay using a DataTrigger:

 

<DataTemplate
    DataType="{x:Type sysreflect:MethodInfo}">
    <DockPanel
        LastChildFill="True">
        <Canvas
            Height="16"
            Width="16">
            <Image
                Height="16"
                Width="16"
                Source="Method.png" />
            <Image
                Visibility="Collapsed"
                Height="16"
                Width="16"
                Source="Key.png"
                Name="ProtectedImage" />
            <Image
                Visibility="Collapsed"
                Height="16"
                Width="16"
                Source="private.png"
                Name="PrivateImage" />
        </Canvas>
        <StackPanel
            Orientation="Vertical"
            HorizontalAlignment="Stretch">
            <TextBlock
                Text="{Binding Path=Name, Mode=OneTime, FallbackValue=Method}"
                FontWeight="Bold"
                VerticalAlignment="Center" />
      </StackPanel>
    </DockPanel>
    <DataTemplate.Triggers>
        <DataTrigger
            Binding="{Binding Path=IsFamily, Mode=OneTime}"
            Value="True">
            <Setter
                Property="Visibility"
                Value="Visible"
                TargetName="ProtectedImage" />
        </DataTrigger>
        <DataTrigger
            Binding="{Binding Path=IsPrivate, Mode=OneTime}"
            Value="True">
            <Setter
                Property="Visibility"
                Value="Visible"
                TargetName="PrivateImage" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

 

The DataTemplate directly maps to the MethodInfo data type. If the IsPrivate property of the MethodInfo class is True, the Private image is shown. The private image is just a small icon that overlays the other image rather than having multiple combinations of images necessary to represent all of the different states.

Some of the challenges were managing the horizontal list so that it always reflected all of the highlighted elements.

The main UI for the application is extremely simple:

<ListBox
    Grid.Row="1"
    Margin="8"
    x:Name="lbClasses"
    ItemsSource="{Binding Path=Data}"
    VerticalContentAlignment="Stretch"
    Style="{DynamicResource ContainerListBoxStyle}"
    ScrollViewer.VerticalScrollBarVisibility="Disabled">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel
                Orientation="Horizontal"
                IsItemsHost="True"
                CanHorizontallyScroll="True"
                CanVerticallyScroll="False" />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

A list box that is bound to a property called data. Everything else is created using Styles and DataTemplates! By placing the ListBox in a DockPanel and setting the CanVerticallyScroll property to False, the contained list boxes (with the details of the assemblies and classes) scroll independently as each is the height of the main window (rather than each ListBox being a unique height creating a jagged user interface).

The application code has to do a bit of hunting when an item is clicked. The tough part is finding the containing ListBox and removing unnecessary data columns. If you have a ListBoxItem and want to find the parent ListBox, you can use the ItemsControl.ItemsControlFromItemContainer static method.

 

private void ComboCollectionListBox_SelectionChanged(object sender, 
SelectionChangedEventArgs e) { foreach (object o in e.AddedItems) { FrameworkElement fe = e.Source as FrameworkElement; if (fe != null) {

DependencyObject d = (DependencyObject)fe;
while (d != null && d.GetType() != typeof(ListBoxItem ))
{
d = VisualTreeHelper.GetParent(d);
}
ListBoxItem lbi = d as ListBoxItem;
ListBox parentListBox = ItemsControl.ItemsControlFromItemContainer(lbi)
as ListBox;
int depth = parentListBox.Items.IndexOf(lbi.DataContext);

for (int i = _data.Count - 1; i > depth; i--)
{
_data.RemoveAt(i);
}

_data.Add(o);
}
}
}


The filtering right now is simple:

CollectionView cv = (CollectionView)
CollectionViewSource.GetDefaultView(lb.DataContext);
if (cv != null)
{
if (cv.CanFilter)
{
cv.Filter = delegate(object o)
{
if (o != null)
{
string title = o.ToString();
if (!string.IsNullOrEmpty(title) &&
title.IndexOf(((TextBox) sender).Text,
StringComparison.InvariantCultureIgnoreCase) >= 0)
{
return true;
}
}
return false;
};
}
}

Once the listbox has been located (lb), it retrieves the CollectionView wrapping the data (which is automatically added by WPF), and then sets the filter. The filter just checks whether the title of each element contains the filter text specified by the user in the text box as each character is typed.


By no means is this a complete object browser. There's many details you'd want to add to make it generally useful.


The source code is available here. I used Visual Studio 2008 -- but no 3.5 features (if you're using an older version of VS, you'll need to recreate the project. All the necessary files are included).

Comments

Nice post/utility!

That's a nice use of DataTemplates and serves as a great reference.

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)

Help support my web site by searching and buying through Amazon.com (in assocation with Amazon.com).