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.
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:
I clicked on the first node:
And so on:
Clicking around changes the various lists:
Support for individual per-column filtering is included:
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:
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:
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).