|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Introduction
Bottom line was that the purely data-driven UI didn't quite work in my case — I needed an alternative. The result was a composite control I built around Contents
Features / Sample ApplicationHere's the most important features at a glance:
Pretty much everything I am going to write here is illustrated in the sample application that comes with the download. If you're already tired of reading, you might just download the sample, play around, and come back later if you need more information. Implementing your Tree ControlRegarding terms: Actually, this is the "hardcodet.net WPF TreeView Control," as I published the initial version on my blog. However, I won't write that all over the place because that just sounds horribly narcistic, and it doesn't really make sense here at CP anyway. As I'll refer to WPF's built-in Extending TreeViewBase<T>All the features of V-Tree are being provided by the abstract
There are quite a few virtual methods you can override in order to control the tree's behavior, but these three abstract methods may well be everything you'll ever need. Below is the implementation of the sample project's //a tree control that handles ShopCategory objects
public class CategoryTree : TreeViewBase<ShopCategory>
{
//the sample uses the category's name as the identifier
public override string GetItemKey(ShopCategory item)
{
return item.CategoryName;
}
//returns subcategories that should be available through the tree
public override ICollection<ShopCategory> GetChildItems(ShopCategory parent)
{
return parent.SubCategories;
}
//get the parent category, or null if it's a root category
public override ShopCategory GetParentItem(ShopCategory item)
{
return item.ParentCategory;
}
}
Hint: Make sure your tree control class is What if I want to bind items of various types to the tree?If you want to bind heterogeneous data to the tree, chances are high that your bound items do have something in common, and having them implement a common interface or derive from a custom base class might be a good design decision anyway. However, nobody keeps you from implementing a tree control that works on items of type //A tree that supports completely different items.
public class MyObjectTree : TreeViewBase<object>
{
public override string GetItemKey(object item)
{
if (item is ShopCategory)
{
return ((ShopCategory) item).CategoryName;
}
else if (item is Vendor)
{
return ((Vendor)item).VendorId;
}
else
{
...
}
}
public override ICollection<object> GetChildItems(object parent)
{
//some implementation
}
public override object GetParentItem(object item)
{
//some implementation
}
}
Basic StructureAll code samples I am going to use refer to the sample implementation of the abstract
Working with the ControlThis section covers the features of the control. As a reference, below is a sample declaration of the tree. Note that some of the properties below are redundant (e.g. <local:CategoryTree x:Name="MyTree"
Items="{Binding Source={StaticResource Shop}, Path=Categories}"
IsLazyLoading="True"
ObserveChildItems="True"
ClearCollapsedNodes="True"
AutoCollapse="True"
RootNode="{StaticResource CustomRootNode}"
NodeContextMenu="{StaticResource CategoryMenu}"
TreeNodeStyle="{StaticResource SimpleFolders}"
TreeStyle="{StaticResource SimpleTreeStyle}"
NodeSortDescriptions="{StaticResource AscendingNames}"
PreserveLayoutOnRefresh="True"
SelectedItem="{Binding ElementName=Foo,
Path=Bar, Mode=TwoWay}"
SelectedItemChanged="OnSelectedItemChanged"
/>
Setting the Items PropertyIn order to display any data, the tree's <local:CategoryTree x:Name="MyTree"
Items="{Binding Source={StaticResource Shop}, Path=Categories}"
What happens now, is this:
It doesn't look too sexy yet, but it's clearly a tree:
Selected ItemsSelecting items was a bit cumbersome with the legacy public virtual T SelectedItem
{
get { return (T) GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
Next to the public void Test()
{
CategoryTree tree = new CategoryTree();
tree.SelectedItemChanged += Tree_SelectedItemChanged;
}
private void Tree_SelectedItemChanged(object sender, RoutedTreeItemEventArgs
Multiple SelectionsJust like the built-in Lazy Loading, Caching, and Memory ConsumptionImagine you'd want to display hierarchical data in two different scenarios:
These scenarios require fundamentally different concepts when it comes to managing tree nodes. In the first scenario, it would be desirable to start with an empty tree, and create tree nodes on demand (lazy loading): As soon as a node is being expanded. If a tree node is being collapsed, all child nodes should be disposed and recreated again if the user re-expands the node. While lazy loading might be an option for the second scenario as well, disposing data is nothing you would want to do if — refetching the data would be far too expensive. Data that was retrieved from the web service should be cached locally in order to prevent repeated requests for the same data over and over again. The control provides two dependency properties to support both scenarios:
Attention: Even if protected virtual bool HasChildItems(T parent)
{
return GetChildItems(parent).Count > 0;
}
However, this only applies for the visible nodes, and does not cause the tree to traverse the whole hierarchy. However, if AutoCollapse Dependency PropertyThis boolean dependency property might come in handy if you want the tree to always display the minimum amount of nodes possible. If set to true, the tree collapses all nodes that do not need to be expanded in order to display the currently selected item. Check the sample application in order to see the feature. SampleThe sample application allows you to set these properties independently and shows the number of tree nodes that are currently in memory at the bottom of the tree control. Below is the same tree twice after a full expansion/collapse cyle: in the first screenshot,
Child Collection Monitoring vs. RefreshChild Collection MonitoringPer default, the tree tries to monitor the child collections of all existing tree nodes for changes, even if they are being collapsed. This ensures that the UI always reflects the correct state (e.g. by removing the expander of a collapsed tree node if all its child nodes are being deleted). Important: Your implementation of You can explicitly disable collection monitoring by setting the tree's Explicitly Refreshing the TreeIf the tree can't or shouldn't update itself, you can invoke one of the V-Tree's private void CopyTreeLayout(object sender, RoutedEventArgs e)
{
//get layout from tree A
TreeLayout layout = CategoryTree.GetTreeLayout();
//assign layout to tree B
SynchronizedTree.Refresh(layout);
}
Styling the TreeV-Tree provides styling point on 3 levels: Bound items, Tree nodes, and the tree itself. Data TemplatesThis is the classic WPF way: Rather than styling an UI element ( <Window.Resources>
<!--
A data template for bound ShopCategory items:
Shows a static folder image and the category name
-->
<DataTemplate DataType="{x:Type shop:ShopCategory}">
<StackPanel Orientation="Horizontal">
<Image Source="/Images/WinFolder.gif" />
<TextBlock Text="{Binding Path=CategoryName}"
Margin="2,0,0,0" />
</StackPanel>
</DataTemplate>
</Window.Resources>
Node StylesYou can explicitly assign a style to all your tree nodes by setting the tree's <local:CategoryTree TreeNodeStyle="{StaticResource SimpleFolders}" />
<Style x:Key="SimpleFolders"
TargetType="{x:Type TreeViewItem}">
<Style.Resources>
<!-- override default brushes that show ugly background colors -->
<Brush x:Key="{x:Static SystemColors.HighlightBrushKey}">Transparent</Brush>
<Brush x:Key="{x:Static SystemColors.ControlBrushKey}">Transparent</Brush>
</Style.Resources>
<!-- everything else is done via the data template -->
<Setter Property="HeaderTemplate"
Value="{StaticResource CategoryTemplate}" />
</Style>
Note that the style above still uses a data template by setting the Btw: If you want to customize some of your nodes completely differently than others, and doing it in XAML is not an option, you can override the protected override void ApplyNodeStyle(TreeViewItem treeNode, ShopCategory item)
{
if (IsCheckableCategory(item))
{
//render the node with a checkbox
ApplyCheckBoxStyle(treeNode);
}
else
{
//just apply the default style
base(ApplyNodeStyle(treeNode, item);
}
}
Tree StyleInternally, the V-TreeView operates on a default <local:CategoryTree TreeStyle="{StaticResource SimpleTreeStyle}" />
<!-- set the tree's background and border properties -->
<Style x:Key="SimpleTreeStyle" TargetType="{x:Type TreeView}">
<Setter Property="Background" Value="#AAA" />
<Setter Property="BorderThickness" Value="4" />
<Setter Property="BorderBrush" Value="#FFA6AAAB" />
</Style>
SortingYou can sort your data easily by setting an <local:CategoryTree NodeSortDescriptions="{StaticResource AscendingNames}" />
The sample application allows you to toggle between two collections which are both declared in XAML. Both sort the tree's <!-- sorts categories by names in ascending order -->
<cm:SortDescriptionCollection x:Key="AscendingNames">
<cm:SortDescription PropertyName="Header.CategoryName"
Direction="Ascending" />
</cm:SortDescriptionCollection>
<!-- sorts categories by names in descending order -->
<cm:SortDescriptionCollection x:Key="DescendingNames">
<cm:SortDescription PropertyName="Header.CategoryName"
Direction="Descending" />
</cm:SortDescriptionCollection>
As with styles, there is a virtual method you can override to customize sorting of specific nodes. The example below would skip sorting of the bound root items: protected override void ApplySorting(TreeViewItem node, ShopCategory item)
{
//only apply sorting for nested items (keep root item order)
if (item.ParentCategory != null)
{
base.ApplySorting(node, item);
}
}
Custom Root NodesThe tree provides a
<!-- a custom root node for the tree -->
<TreeViewItem x:Key="CustomRootNode">
<TreeViewItem.Header>
...
</TreeViewItem.Header>
</TreeViewItem>
<local:CategoryTree Items="{Binding Source={StaticResource Shop}, Path=Categories}"
RootNode="{StaticResource CustomRootNode}" />
Context MenusIf you want to display a specific context menu on all your nodes, you can set the tree's <local:CategoryTree NodeContextMenu="{StaticResource CategoryMenu}" />
You'll probably use WPF's command system to trigger menu click events. The sample application provides a context menu that allows the user to add new categories or delete existing ones, if they aren't root categories. For the sample, I used the built-in application commands <Window.Resources>
<!-- the context menu for the tree -->
<ContextMenu x:Key="CategoryMenu">
<MenuItem Header="Add Subcategory"
Command="New">
<MenuItem.Icon>
<Image Source="/Images/Add.png" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Remove Category"
Command="Delete">
<MenuItem.Icon>
<Image Source="/Images/Remove.png" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</Window.Resources>
In order to handle commands, MainWindow.xaml (not the tree control!) declares event handlers for both commands: <Window.CommandBindings>
<!-- bindings for context menu commands -->
<CommandBinding Command="New"
Executed="AddCategory" />
<CommandBinding Command="Delete"
CanExecute="EvaluateCanDelete"
Executed="DeleteCategory" />
</Window.CommandBindings>
And here's the code. The important thing here is
/// <summary>/// Determines the item that is the source of a given command.
/// As a command event can be routed from a context menu click
/// or a short-cut, we have to evaluate both possibilities.
/// </summary>
///// <returns></returns>
private ShopCategory GetCommandItem()
{
//get the processed item
ContextMenu menu = CategoryTree.NodeContextMenu;
if (menu.IsVisible)
{
//a context menu was clicked
TreeViewItem treeNode = (TreeViewItem) menu.PlacementTarget;
return (ShopCategory) treeNode.Header;
}
else
{
//the context menu is closed - the user has pressed a shortcut
//-> the command was triggered from the currently selected item
return CategoryTree.SelectedItem;
}
}
/// <summary>
/// Creates a sub category for the clicked item
/// and refreshes the tree.
/// </summary>
private void AddCategory(object sender, ExecutedRoutedEventArgs e)
{
//get the processed item
ShopCategory parent = GetCommandItem();
...
//mark the event as handled
e.Handled = true;
}
/// <summary>
/// Checks whether it is allowed to delete a category, which is only
/// allowed for nested categories, but not the root items.
/// </summary>
private void EvaluateCanDelete(object sender, CanExecuteRoutedEventArgs e)
{
//get the processed item
ShopCategory item = GetCommandItem();
e.CanExecute = item.ParentCategory != null;
e.Handled = true;
}
/// <summary>
/// Deletes the currently processed item. This can be a right-clicked
/// item (context menu) or the currently selected item, if the user
/// pressed delete.
/// </summary>
private void DeleteCategory(object sender, ExecutedRoutedEventArgs e)
{
//get item
ShopCategory item = GetCommandItem();
//remove from parent
item.ParentCategory.SubCategories.Remove(item);
//mark the event as handled
e.Handled = true;
}
Selecting Items before Displaying a Context MenuIf you want the tree to automatically select nodes that are being right-clicked, you can set the Updates and NewsletterThis is the first control I've written for WPF, and I'm sure that there will be a few bugs to squash as well as a few nice improvements, both hopefully based on your critical feedback (after all, it's all about learning). So check back for updates and keep the suggestions coming Future releases of the control will be available here at CodeProject, and I'll also post about them on my blog. If you want to get automatically notified of relevant updates, you can subscribe to a specific newsletter here: http://www.hardcodet.net/newsletter. Just register, then select the control from the list of available newsletters. I hope you'll find that the control makes a useful addition to your toolbox - have fun! Changelog1.0.5 Initial public release (2008.01.29) | ||||||||||||||||||||