// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
using System.Windows.Markup;
namespace System.Windows.Controls.DataVisualization
{
///
/// Represents a control which can display hierarchical data as a set of nested rectangles.
/// Each item in the hierarchy is laid out in a rectangular area of a size proportional to
/// the value associated with the item.
///
///
/// You populate a TreeMap by setting its ItemsSource property to the root of the hierarchy
/// you would like to display. The ItemDefinition property must be set to an instance of a
/// TreeMapItemDefinition with appropriate bindings for Value (identifying the value to be used
/// when calculating relative item sizes) and ItemsSource (identifying the collection of
/// children for each item).
///
/// Preview
[TemplatePart(Name = ContainerName, Type = typeof(Canvas))]
[ContentProperty("ItemsSource")]
public class TreeMap : Control
{
///
/// The name of the Container template part.
///
private const string ContainerName = "Container";
#region private object InterpolatorValue
///
/// Identifies the InterpolatorValue dependency property.
///
private static readonly DependencyProperty InterpolatorValueProperty =
DependencyProperty.Register(
"InterpolatorValue",
typeof(object),
typeof(TreeMap),
null);
///
/// Gets or sets a generic value used as a temporary storage used as a source for TargetName/TargetProperty binding.
///
private object InterpolatorValue
{
get { return (object)GetValue(InterpolatorValueProperty); }
set { SetValue(InterpolatorValueProperty, value); }
}
#endregion
///
/// Holds a helper object used to extract values using a property path.
///
private BindingExtractor _helper;
///
/// The roots of the pre-calculated parallel tree of TreeMapNodes.
///
private IEnumerable _nodeRoots;
///
/// Cached sequence of all TreeMapNodes used by GetTreeMapNodes.
///
private IEnumerable _getTreeMapNodesCache;
#region public TreeMapItemDefinitionSelector TreeMapItemDefinitionSelector
///
/// Gets or sets the selector used to choose the item template dynamically.
///
public TreeMapItemDefinitionSelector ItemDefinitionSelector
{
get { return (TreeMapItemDefinitionSelector)GetValue(ItemDefinitionSelectorProperty); }
set { SetValue(ItemDefinitionSelectorProperty, value); }
}
///
/// Identifies the ItemDefinitionSelector dependency property.
///
public static readonly DependencyProperty ItemDefinitionSelectorProperty =
DependencyProperty.Register(
"ItemDefinitionSelector",
typeof(TreeMapItemDefinitionSelector),
typeof(TreeMap),
new PropertyMetadata(OnItemDefinitionSelectorPropertyChanged));
///
/// Called when the value of the TreeMapItemDefinitionSelectorProperty property changes.
///
/// Reference to the TreeMap object.
/// Event handler arguments.
private static void OnItemDefinitionSelectorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TreeMap treeMap = d as TreeMap;
if (treeMap != null)
{
TreeMapItemDefinitionSelector oldValue = e.OldValue as TreeMapItemDefinitionSelector;
TreeMapItemDefinitionSelector newValue = e.NewValue as TreeMapItemDefinitionSelector;
treeMap.OnItemDefinitionSelectorPropertyChanged(oldValue, newValue);
}
}
///
/// Called when the value of the ItemDefinitionSelectorProperty property changes.
/// Triggers a recalculation of the layout.
///
/// The old selector.
/// The new selector.
protected virtual void OnItemDefinitionSelectorPropertyChanged(TreeMapItemDefinitionSelector oldValue, TreeMapItemDefinitionSelector newValue)
{
RebuildTree();
}
#endregion
#region public TreeMapItemDefinition ItemDefinition
///
/// Gets or sets a value representing the template used to display each item.
///
public TreeMapItemDefinition ItemDefinition
{
get { return (TreeMapItemDefinition)GetValue(ItemDefinitionProperty); }
set { SetValue(ItemDefinitionProperty, value); }
}
///
/// Identifies the ItemDefinition dependency property.
///
public static readonly DependencyProperty ItemDefinitionProperty =
DependencyProperty.Register(
"ItemDefinition",
typeof(TreeMapItemDefinition),
typeof(TreeMap),
new PropertyMetadata(OnItemDefinitionPropertyChanged));
///
/// Called when the value of the ItemDefinitionProperty property changes.
///
/// Reference to the TreeMap object.
/// Event handler arguments.
private static void OnItemDefinitionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TreeMap treeMap = d as TreeMap;
if (treeMap != null)
{
TreeMapItemDefinition oldValue = e.OldValue as TreeMapItemDefinition;
TreeMapItemDefinition newValue = e.NewValue as TreeMapItemDefinition;
// Unregister old TreeMapItemDefinition
if (oldValue != null)
{
oldValue.PropertyChanged -= treeMap.OnItemDefinitionPropertyChanged;
}
// Register new TreeMapItemDefinition
if (newValue != null)
{
newValue.PropertyChanged += treeMap.OnItemDefinitionPropertyChanged;
}
treeMap.OnItemDefinitionPropertyChanged(oldValue, newValue);
}
}
///
/// This callback ensures that any change in TreeMapItemDefinition.
///
/// Source TreeMapItemDefinition object.
/// Event handler arguments (parameter name).
private void OnItemDefinitionPropertyChanged(object sender, PropertyChangedEventArgs e)
{
RebuildTree();
}
///
/// Called when the value of the ItemDefinitionProperty property changes.
/// Triggers a recalculation of the layout.
///
/// The old item definition.
/// The new item definition.
protected virtual void OnItemDefinitionPropertyChanged(TreeMapItemDefinition oldValue, TreeMapItemDefinition newValue)
{
RebuildTree();
}
#endregion
#region public IEnumerable ItemsSource
///
/// Gets or sets a value representing the list of hierarchies used to generate
/// content for the TreeMap.
///
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
///
/// Identifies the ItemsSource dependency property.
///
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource",
typeof(IEnumerable),
typeof(TreeMap),
new PropertyMetadata(OnItemsSourcePropertyChanged));
///
/// Called when the value of the ItemsSourceProperty property changes.
///
/// Reference to the TreeMap object.
/// Event handler arguments.
private static void OnItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TreeMap treeMap = d as TreeMap;
if (treeMap != null)
{
IEnumerable oldValue = e.OldValue as IEnumerable;
IEnumerable newValue = e.NewValue as IEnumerable;
treeMap.OnItemsSourcePropertyChanged(oldValue, newValue);
}
}
///
/// Called when the value of the ItemsSourceProperty property changes.
///
/// The old ItemsSource collection.
/// The new ItemsSource collection.
protected virtual void OnItemsSourcePropertyChanged(IEnumerable oldValue, IEnumerable newValue)
{
// Remove handler for oldValue.CollectionChanged (if present)
INotifyCollectionChanged oldValueINotifyCollectionChanged = oldValue as INotifyCollectionChanged;
if (null != oldValueINotifyCollectionChanged)
{
// Detach the WeakEventListener
if (null != _weakEventListener)
{
_weakEventListener.Detach();
_weakEventListener = null;
}
}
// Add handler for newValue.CollectionChanged (if possible)
INotifyCollectionChanged newValueINotifyCollectionChanged = newValue as INotifyCollectionChanged;
if (null != newValueINotifyCollectionChanged)
{
// Use a WeakEventListener so that the backwards reference doesn't keep this object alive
_weakEventListener = new WeakEventListener(this);
_weakEventListener.OnEventAction = (instance, source, eventArgs) => instance.ItemsSourceCollectionChanged(source, eventArgs);
_weakEventListener.OnDetachAction = (weakEventListener) => newValueINotifyCollectionChanged.CollectionChanged -= weakEventListener.OnEvent;
newValueINotifyCollectionChanged.CollectionChanged += _weakEventListener.OnEvent;
}
// Handle property change
RebuildTree();
}
///
/// WeakEventListener used to handle INotifyCollectionChanged events.
///
private WeakEventListener _weakEventListener;
///
/// Method that handles the ObservableCollection.CollectionChanged event for the ItemsSource property.
///
/// The object that raised the event.
/// The event data.
private void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RebuildTree();
}
#endregion
#region private Collection Interpolators
///
/// Gets or sets a value representing a collection of interpolators to use in TreeMap.
///
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Setter is public to work around a limitation with the XAML editing tools.")]
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value", Justification = "Setter is public to work around a limitation with the XAML editing tools.")]
public Collection Interpolators
{
get { return (Collection)GetValue(InterpolatorsProperty); }
set { throw new NotSupportedException(Properties.Resources.TreeMap_Interpolators_SetterNotSupported); }
}
///
/// Identifies the Interpolators dependency property.
///
public static readonly DependencyProperty InterpolatorsProperty =
DependencyProperty.Register(
"Interpolators",
typeof(Collection),
typeof(TreeMap),
new PropertyMetadata(OnInterpolatorsPropertyChanged));
///
/// Called when the value of the InterpolatorsProperty property changes.
///
/// Reference to the TreeMap object.
/// Event handler arguments.
private static void OnInterpolatorsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TreeMap treeMap = d as TreeMap;
if (treeMap != null)
{
Collection oldValue = e.OldValue as Collection;
Collection newValue = e.NewValue as Collection;
treeMap.OnInterpolatorsPropertyChanged(oldValue, newValue);
}
}
///
/// Called when the value of the InterpolatorsProperty property changes.
/// Triggers a recalculation of the layout.
///
/// The old Interpolators collection.
/// The new Interpolators collection.
protected virtual void OnInterpolatorsPropertyChanged(Collection oldValue, Collection newValue)
{
RebuildTree();
}
#endregion
#region Template Parts
///
/// The Container template part is used to hold all the items inside
/// a TreeMap.
///
private Canvas _containerElement;
///
/// Gets the Container template part that is used to hold all the items inside
/// a TreeMap.
///
internal Canvas ContainerElement
{
get { return _containerElement; }
private set
{
// Detach from the old Container element
if (_containerElement != null)
{
_containerElement.Children.Clear();
}
// Attach to the new Container element
_containerElement = value;
}
}
#endregion
///
/// Initializes a new instance of the TreeMap class.
///
public TreeMap()
{
_helper = new BindingExtractor();
DefaultStyleKey = typeof(TreeMap);
SetValue(InterpolatorsProperty, new ObservableCollection());
(Interpolators as ObservableCollection).CollectionChanged +=
new NotifyCollectionChangedEventHandler(OnInterpolatorsCollectionChanged);
}
///
/// Invoked whenever application code or internal processes call ApplyTemplate. Gets references
/// to the template parts required by this control.
///
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
ContainerElement = GetTemplateChild(ContainerName) as Canvas;
RebuildTree();
}
///
/// Constructs a new instance of an element used to display an item in the tree.
///
///
/// By default TreeMap will use the template set in its ItemDefinition property, or the value
/// returned from GetTemplateForItemOverride if overridden. Override this method to build a
/// custom element.
///
/// One of the items in the ItemsSource hierarchy.
/// The level of the item in the hierarchy.
/// A new FrameworkElement which will be added to the TreeMap control. If this
/// method returns null the TreeMap will create the item using the ItemDefinition property,
/// or the value returned by TreeMapItemDefinitionSelector if specified.
protected virtual FrameworkElement GetContainerForItemOverride(object data, int level)
{
return null;
}
///
/// Performs the Arrange pass of the layout.
///
///
/// We round rectangles to snap to nearest pixels. We do that to avoid
/// anti-aliasing which results in better appearance. Moreover to get
/// correct layout we would need to use UseLayoutRounding=false which
/// is Silverlight specific. A side effect is that areas for rectangles
/// in the visual tree no longer can be used to compare them as dimensions
/// are not rounded and therefore not precise.
///
/// The final area within the parent that this element should use to arrange itself and its children.
/// The actual size used.
protected override Size ArrangeOverride(Size finalSize)
{
// Sets ActualHeight & ActualWidth for the container
finalSize = base.ArrangeOverride(finalSize);
if (_nodeRoots != null && ContainerElement != null)
{
// Create a temporary pseudo-root node containing all the top-level nodes
TreeMapNode root = new TreeMapNode()
{
Area = _nodeRoots.Sum(x => x.Area),
Children = _nodeRoots,
ChildItemPadding = new Thickness(0)
};
// Calculate associated rectangles. We use ContainerElement,
// not finalSize so all elements that are above it like border
// (with padding and border) are taken into account
IEnumerable> measuredRectangles = ComputeRectangles(
root,
new Rect(0, 0, ContainerElement.ActualWidth, ContainerElement.ActualHeight));
// Position everything
foreach (Tuple rectangle in measuredRectangles)
{
FrameworkElement element = rectangle.Item2.Element;
if (element != null)
{
double roundedTop = Math.Round(rectangle.Item1.Top);
double roundedLeft = Math.Round(rectangle.Item1.Left);
double height = Math.Round(rectangle.Item1.Height + rectangle.Item1.Top) - roundedTop;
double width = Math.Round(rectangle.Item1.Width + rectangle.Item1.Left) - roundedLeft;
// Fully specify element location/size (setting size is required on WPF)
Canvas.SetLeft(element, roundedLeft);
Canvas.SetTop(element, roundedTop);
element.Width = width;
element.Height = height;
element.Arrange(new Rect(roundedLeft, roundedTop, width, height));
}
}
}
return finalSize;
}
///
/// Triggers a recalculation of the layout when items are added/removed from the Interpolators collection.
///
/// Reference to the Interpolators collection.
/// Event handler arguments.
private void OnInterpolatorsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RebuildTree();
}
///
/// Returns a sequence of TreeMapNodes in breadth-first order.
///
/// Sequence of TreeMapNodes.
private IEnumerable GetTreeMapNodes()
{
if (_getTreeMapNodesCache == null)
{
// Create a new list
List allNodes = new List();
// Seed the queue with the roots
Queue nodes = new Queue();
foreach (TreeMapNode node in _nodeRoots ?? Enumerable.Empty())
{
nodes.Enqueue(node);
}
// Process the queue in breadth-first order
while (0 < nodes.Count)
{
TreeMapNode node = nodes.Dequeue();
allNodes.Add(node);
foreach (TreeMapNode child in node.Children)
{
nodes.Enqueue(child);
}
}
// Cache the list
_getTreeMapNodesCache = allNodes;
}
// Return the cached sequence
return _getTreeMapNodesCache;
}
///
/// Recursively computes TreeMap rectangles given the root node and the bounding rectangle as start.
///
/// Root of the TreeMapNode tree.
/// Bounding rectangle which will be sub-divided.
/// A list of RectangularAreas containing a rectangle for each node in the tree.
private IEnumerable> ComputeRectangles(TreeMapNode root, Rect boundingRectangle)
{
Queue> treeQueue = new Queue>();
treeQueue.Enqueue(new Tuple(boundingRectangle, root));
// Perform a breadth-first traversal of the tree
SquaringAlgorithm algorithm = new SquaringAlgorithm();
while (treeQueue.Count > 0)
{
Tuple currentParent = treeQueue.Dequeue();
yield return currentParent;
foreach (Tuple rectangle in
algorithm.Split(currentParent.Item1, currentParent.Item2, currentParent.Item2.ChildItemPadding))
{
treeQueue.Enqueue(rectangle);
}
}
}
///
/// Builds the parallel trees of TreeMapNodes with references to the original user's trees.
///
/// The list of roots of the user hierarchies (whatever was passed through ItemsSource).
/// Level being processed at this recursive call (the root node is at level 0).
/// The list of roots of the internal trees of TreeMapNodes.
private IEnumerable BuildTreeMapTree(IEnumerable nodes, int level)
{
List retList = new List();
if (nodes == null)
{
return retList;
}
foreach (object root in nodes)
{
// Give the template selector a chance to override the template for this item.
TreeMapItemDefinition template = null;
if (ItemDefinitionSelector != null)
{
template = ItemDefinitionSelector.SelectItemDefinition(this, root, level);
}
// Use the default otherwise
if (template == null)
{
template = ItemDefinition;
}
if (template == null)
{
throw new ArgumentException(
Properties.Resources.TreeMap_BuildTreeMapTree_TemplateNotSet);
}
// Silently create 0 elements if ValueBinding is set to null
// in the template
if (template.ValueBinding != null)
{
IEnumerable objectChildren = (template.ItemsSource != null) ?
_helper.RetrieveProperty(root, template.ItemsSource) as IEnumerable :
null;
IEnumerable children = (objectChildren != null) ?
BuildTreeMapTree(objectChildren, level + 1) :
children = Enumerable.Empty();
// Subscribe to CollectionChanged for the collection
WeakEventListener weakEventListener = null;
INotifyCollectionChanged objectChildrenINotifyCollectionChanged = objectChildren as INotifyCollectionChanged;
if (objectChildrenINotifyCollectionChanged != null)
{
// Use a WeakEventListener so that the backwards reference doesn't keep this object alive
weakEventListener = new WeakEventListener(this);
weakEventListener.OnEventAction = (instance, source, eventArgs) => instance.ItemsSourceCollectionChanged(source, eventArgs);
weakEventListener.OnDetachAction = (wel) => objectChildrenINotifyCollectionChanged.CollectionChanged -= wel.OnEvent;
objectChildrenINotifyCollectionChanged.CollectionChanged += weakEventListener.OnEvent;
}
// Auto-aggregate children area values
double area;
if (children.Any())
{
area = children.Sum(x => x.Area);
}
else
{
IConvertible value = _helper.RetrieveProperty(root, template.ValueBinding) as IConvertible;
if (value == null)
{
// Provide a default value so there's something to display
value = 1.0;
}
area = value.ToDouble(CultureInfo.InvariantCulture);
}
// Do not include elements with negative or 0 size in the
// VisualTransition tree. We skip interpolation for such
// elements as well
if (area > 0)
{
// Calculate ranges for all interpolators, only consider leaf
// nodes in the LeafNodesOnly mode, or all nodes in the AllNodes
// mode.
foreach (Interpolator interpolator in Interpolators)
{
if (interpolator.InterpolationMode == InterpolationMode.AllNodes || !children.Any())
{
interpolator.IncludeInRange(root);
}
}
retList.Add(new TreeMapNode()
{
DataContext = root,
Level = level,
Area = area,
ItemDefinition = template,
ChildItemPadding = template.ChildItemPadding,
Children = children,
WeakEventListener = weakEventListener,
});
}
}
}
return retList;
}
///
/// Extracts all children from the user's trees (ItemsSource) into a flat list, and
/// creates UI elements for them.
///
private void CreateChildren()
{
// Breadth-first traversal so elements closer to the root will be added first,
// so that leaf elements will show on top of them.
foreach (TreeMapNode current in GetTreeMapNodes())
{
// Create the UI element and keep a reference to it in our tree
FrameworkElement element = GetContainerForItemOverride(current.DataContext, current.Level);
if (element == null && current.ItemDefinition.ItemTemplate != null)
{
element = current.ItemDefinition.ItemTemplate.LoadContent() as FrameworkElement;
}
// If an element was created
if (element != null)
{
current.Element = element;
// Apply interpolators to element
foreach (Interpolator interpolator in Interpolators)
{
// Apply interpolators only for leaf nodes in the
// LeafNodesOnly mode, or for all nodes in the AllNodes
// mode.
if (interpolator.InterpolationMode == InterpolationMode.AllNodes || !current.Children.Any())
{
DependencyObject target = element.FindName(interpolator.TargetName) as DependencyObject;
if (target != null)
{
SetBinding(
InterpolatorValueProperty,
new Binding(interpolator.TargetProperty) { Source = target, Mode = BindingMode.TwoWay });
if (interpolator.DataRangeBinding == null)
{
throw new ArgumentException(
Properties.Resources.TreeMap_CreateChildren_InterpolatorBindingNotSet);
}
// Extract the current value to interpolate
IConvertible value =
_helper.RetrieveProperty(current.DataContext, interpolator.DataRangeBinding) as
IConvertible;
if (value == null)
{
throw new ArgumentException(
Properties.Resources.Interpolator_IncludeInRange_DataRangeBindingNotIConvertible);
}
// This will update the TargetProperty of the TargetName object
InterpolatorValue = interpolator.Interpolate(value.ToDouble(CultureInfo.InvariantCulture));
}
}
}
// Add new child to the panel
element.DataContext = current.DataContext;
ContainerElement.Children.Add(element);
}
}
}
///
/// Called internally whenever a property of TreeMap is changed and the internal
/// structures need to be rebuilt in order to recalculate the layout.
///
private void RebuildTree()
{
if (ContainerElement != null)
{
// Unhook from CollectionChanged
foreach (TreeMapNode treeMapNode in GetTreeMapNodes().Where(n => n.WeakEventListener != null))
{
treeMapNode.WeakEventListener.Detach();
}
// Reset all interpolators
foreach (Interpolator interpolator in Interpolators)
{
interpolator.ActualDataMinimum = double.PositiveInfinity;
interpolator.ActualDataMaximum = double.NegativeInfinity;
interpolator.DataContext = this.DataContext;
}
// Build the parallel tree of TreeMapNodes needed by the algorithm
_nodeRoots = BuildTreeMapTree(ItemsSource, 0);
// Clear cache
_getTreeMapNodesCache = null;
// Populate the TreeMap panel with a flat list of all children
// in the hierarchy passed in.
ContainerElement.Children.Clear();
CreateChildren();
// Refresh UI
InvalidateArrange();
}
}
}
}