// (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.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Windows.Controls.DataVisualization.Charting.Primitives; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Markup; using System.Windows.Media; namespace System.Windows.Controls.DataVisualization.Charting { /// /// Implements a series that is defined by one or more instances of the DefinitionSeries class. /// /// Preview [ContentProperty("SeriesDefinitions")] [TemplatePart(Name = SeriesAreaName, Type = typeof(Grid))] [TemplatePart(Name = ItemContainerName, Type = typeof(DelegatingListBox))] [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Class is maintainable.")] public abstract class DefinitionSeries : Control, ISeries, IAxisListener, IRangeProvider, IValueMarginProvider, IDataProvider, ISeriesHost { /// /// Name of the SeriesArea property. /// private const string SeriesAreaName = "SeriesArea"; /// /// Name of the ItemContainer property. /// private const string ItemContainerName = "ItemContainer"; /// /// Gets or sets a value indicating whether the series is 100% stacked (versus normally stacked). /// protected bool IsStacked100 { get; set; } /// /// Gets the collection of DataItems representing the data of the series. /// protected ObservableCollection DataItems { get; private set; } /// /// Gets the SeriesArea template part instance. /// protected Panel SeriesArea { get; private set; } /// /// Stores an aggregated collection of legend items from the series definitions. /// private readonly AggregatedObservableCollection _legendItems = new AggregatedObservableCollection(); /// /// Stores the collection of SeriesDefinitions that define the series. /// private readonly ObservableCollection _seriesDefinitions = new UniqueObservableCollection(); /// /// Stores a mirror collection of ISeries corresponding directly to the collection of SeriesDefinitions. /// /// /// Not using ObservableCollectionListAdapter because of race condition on ItemsChanged event /// private readonly ObservableCollection _seriesDefinitionsAsISeries = new ObservableCollection(); /// /// Keeps the SeriesDefinitions collection synchronized with the Children collection of the SeriesArea. /// private readonly ObservableCollectionListAdapter _seriesAreaChildrenListAdapter = new ObservableCollectionListAdapter(); /// /// Stores the clip geometry for the ItemContainer. /// private readonly RectangleGeometry _clipGeometry = new RectangleGeometry(); /// /// Stores a reference to the ItemContainer template part. /// private DelegatingListBox _itemContainer; /// /// Tracks the collection of DataItem that are queued for update. /// private readonly List _queueUpdateDataItemPlacement_DataItems = new List(); /// /// Tracks whether the dependent axis values changed for the next update. /// private bool _queueUpdateDataItemPlacement_DependentAxisValuesChanged; /// /// Tracks whether the independent axis values changed for the next update. /// private bool _queueUpdateDataItemPlacement_IndependentAxisValuesChanged; /// /// Stores a reference to the backing collection for the SelectedItems property. /// private ObservableCollection _selectedItems = new ObservableCollection(); /// /// Tracks whether the SelectedItems collection is being synchronized (to prevent reentrancy). /// private bool _synchronizingSelectedItems; #if !SILVERLIGHT /// /// Performs one-time initialization of DefinitionSeries data. /// [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Dependency properties are initialized in-line.")] static DefinitionSeries() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DefinitionSeries), new FrameworkPropertyMetadata(typeof(DefinitionSeries))); } #endif /// /// Initializes a new instance of the DefinitionSeries class. /// protected DefinitionSeries() { #if SILVERLIGHT this.DefaultStyleKey = typeof(DefinitionSeries); #endif _seriesDefinitions.CollectionChanged += new NotifyCollectionChangedEventHandler(SeriesDefinitionsCollectionChanged); _seriesAreaChildrenListAdapter.Collection = _seriesDefinitions; _selectedItems.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedItemsCollectionChanged); DataItems = new ObservableCollection(); } /// /// Gets or sets the dependent axis of the series. /// public IAxis DependentAxis { get { return (IAxis)GetValue(DependentAxisProperty); } set { SetValue(DependentAxisProperty, value); } } /// /// Identifies the DependentAxis dependency property. /// public static readonly DependencyProperty DependentAxisProperty = DependencyProperty.Register("DependentAxis", typeof(IAxis), typeof(DefinitionSeries), new PropertyMetadata(OnDependentAxisChanged)); /// /// Handles changes to the DependentAxis dependency property. /// /// DependencyObject that changed. /// Event data for the DependencyPropertyChangedEvent. private static void OnDependentAxisChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { ((DefinitionSeries)o).OnDependentAxisChanged((IAxis)e.OldValue, (IAxis)e.NewValue); } /// /// Handles changes to the DependentAxis property. /// /// Old value. /// New value. [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "oldValue", Justification = "Parameter is part of the pattern for DependencyProperty change handlers.")] [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "newValue", Justification = "Parameter is part of the pattern for DependencyProperty change handlers.")] private void OnDependentAxisChanged(IAxis oldValue, IAxis newValue) { if (null != ActualDependentAxis) { EnsureAxes(true, false, false); } } /// /// Gets or sets the independent axis of the series. /// public IAxis IndependentAxis { get { return (IAxis)GetValue(IndependentAxisProperty); } set { SetValue(IndependentAxisProperty, value); } } /// /// Identifies the IndependentAxis dependency property. /// public static readonly DependencyProperty IndependentAxisProperty = DependencyProperty.Register("IndependentAxis", typeof(IAxis), typeof(DefinitionSeries), new PropertyMetadata(OnIndependentAxisChanged)); /// /// Handles changes to the IndependentAxis dependency property. /// /// DependencyObject that changed. /// Event data for the DependencyPropertyChangedEvent. private static void OnIndependentAxisChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { ((DefinitionSeries)o).OnIndependentAxisChanged((IAxis)e.OldValue, (IAxis)e.NewValue); } /// /// Handles changes to the IndependentAxis property. /// /// Old value. /// New value. [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "oldValue", Justification = "Parameter is part of the pattern for DependencyProperty change handlers.")] [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "newValue", Justification = "Parameter is part of the pattern for DependencyProperty change handlers.")] private void OnIndependentAxisChanged(IAxis oldValue, IAxis newValue) { if (null != ActualIndependentAxis) { EnsureAxes(false, true, false); } } /// /// Gets the rendered dependent axis of the series. /// public IAxis ActualDependentAxis { get { return (IAxis)GetValue(ActualDependentAxisProperty); } protected set { SetValue(ActualDependentAxisProperty, value); } } /// /// Identifies the ActualDependentAxis dependency property. /// public static readonly DependencyProperty ActualDependentAxisProperty = DependencyProperty.Register("ActualDependentAxis", typeof(IAxis), typeof(DefinitionSeries), null); /// /// Gets the rendered independent axis of the series. /// public IAxis ActualIndependentAxis { get { return (IAxis)GetValue(ActualIndependentAxisProperty); } protected set { SetValue(ActualIndependentAxisProperty, value); } } /// /// Identifies the ActualIndependentAxis dependency property. /// public static readonly DependencyProperty ActualIndependentAxisProperty = DependencyProperty.Register("ActualIndependentAxis", typeof(IAxis), typeof(DefinitionSeries), null); /// /// Gets the ActualDependentAxis as an IRangeAxis instance. /// protected IRangeAxis ActualDependentRangeAxis { get { return (IRangeAxis)ActualDependentAxis; } } /// /// Gets the collection of legend items for the series. /// public ObservableCollection LegendItems { get { return _legendItems; } } /// /// Gets or sets the SeriesHost for the series. /// public ISeriesHost SeriesHost { get { return _seriesHost; } set { if (null != _seriesHost) { _seriesHost.ResourceDictionariesChanged -= new EventHandler(SeriesHostResourceDictionariesChanged); if (null != ActualDependentAxis) { ActualDependentAxis.RegisteredListeners.Remove(this); ActualDependentAxis = null; } if (null != ActualIndependentAxis) { ActualIndependentAxis.RegisteredListeners.Remove(this); ActualIndependentAxis = null; } foreach (SeriesDefinition definition in SeriesDefinitions) { SeriesDefinitionItemsSourceChanged(definition, definition.ItemsSource, null); } } _seriesHost = value; SeriesHostResourceDictionariesChanged(null, null); if (null != _seriesHost) { _seriesHost.ResourceDictionariesChanged += new EventHandler(SeriesHostResourceDictionariesChanged); foreach (SeriesDefinition definition in SeriesDefinitions) { SeriesDefinitionItemsSourceChanged(definition, null, definition.ItemsSource); } } } } /// /// Stores the SeriesHost for the series. /// private ISeriesHost _seriesHost; /// /// Gets or sets the collection of SeriesDefinitions that define the series. /// [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 SeriesDefinitions { get { return _seriesDefinitions; } set { throw new NotSupportedException(Properties.Resources.DefinitionSeries_SeriesDefinitions_SetterNotSupported); } } /// /// Gets or sets the SelectionMode property. /// public SeriesSelectionMode SelectionMode { get { return (SeriesSelectionMode)GetValue(SelectionModeProperty); } set { SetValue(SelectionModeProperty, value); } } /// /// Identifies the SelectionMode dependency property. /// public static readonly DependencyProperty SelectionModeProperty = DependencyProperty.Register("SelectionMode", typeof(SeriesSelectionMode), typeof(DefinitionSeries), new PropertyMetadata(SeriesSelectionMode.None, OnSelectionModeChanged)); /// /// Handles changes to the SelectionMode dependency property. /// /// DependencyObject that changed. /// Event data for the DependencyPropertyChangedEvent. private static void OnSelectionModeChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { ((DefinitionSeries)o).OnSelectionModeChanged((SeriesSelectionMode)e.OldValue, (SeriesSelectionMode)e.NewValue); } /// /// Handles changes to the SelectionMode property. /// /// Old value. /// New value. [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "oldValue", Justification = "Parameter is part of the pattern for DependencyProperty change handlers.")] private void OnSelectionModeChanged(SeriesSelectionMode oldValue, SeriesSelectionMode newValue) { if (null != _itemContainer) { switch (newValue) { case SeriesSelectionMode.None: _itemContainer.SelectedItem = null; _itemContainer.SelectionMode = Controls.SelectionMode.Single; break; case SeriesSelectionMode.Single: _itemContainer.SelectionMode = Controls.SelectionMode.Single; break; case SeriesSelectionMode.Multiple: _itemContainer.SelectionMode = Controls.SelectionMode.Multiple; break; } } } /// /// Gets or sets the SelectedIndex property. /// public int SelectedIndex { get { return (int)GetValue(SelectedIndexProperty); } set { SetValue(SelectedIndexProperty, value); } } /// /// Identifies the SelectedIndex dependency property. /// public static readonly DependencyProperty SelectedIndexProperty = DependencyProperty.Register("SelectedIndex", typeof(int), typeof(DefinitionSeries), new PropertyMetadata(-1)); /// /// Gets or sets the SelectedItem property. /// public object SelectedItem { get { return (object)GetValue(SelectedItemProperty); } set { SetValue(SelectedItemProperty, value); } } /// /// Identifies the SelectedItem dependency property. /// public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(object), typeof(DefinitionSeries), null); /// /// Gets the currently selected items. /// /// /// This property is meant to be used when SelectionMode is Multiple. If the selection mode is Single the correct property to use is SelectedItem. /// public IList SelectedItems { get { return _selectedItems; } } /// /// Handles the CollectionChanged event of the SelectedItems collection. /// /// Event source. /// Event arguments. private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (!_synchronizingSelectedItems) { try { _synchronizingSelectedItems = true; // Synchronize the SelectedItems collection if (null != _itemContainer) { if (NotifyCollectionChangedAction.Reset == e.Action) { if (0 < _itemContainer.SelectedItems.Count) { _itemContainer.SelectedItems.Clear(); } foreach (DataItem dataItem in _selectedItems.SelectMany(v => DataItems.Where(di => object.Equals(di.Value, v)))) { _itemContainer.SelectedItems.Add(dataItem); } } else { if (null != e.OldItems) { foreach (DataItem dataItem in e.OldItems.CastWrapper().SelectMany(v => DataItems.Where(di => object.Equals(di.Value, v)))) { _itemContainer.SelectedItems.Remove(dataItem); } } if (null != e.NewItems) { foreach (DataItem dataItem in e.NewItems.CastWrapper().SelectMany(v => DataItems.Where(di => object.Equals(di.Value, v)))) { _itemContainer.SelectedItems.Add(dataItem); } } } } } finally { _synchronizingSelectedItems = false; } } } /// /// Handles the SelectionChanged event of the ItemContainer class. /// /// Event source. /// Event arguments. private void ItemContainerSelectionChanged(object sender, SelectionChangedEventArgs e) { DataItem[] removedDataItems = e.RemovedItems.CastWrapper().ToArray(); DataItem[] addedDataItems = e.AddedItems.CastWrapper().ToArray(); if (!_synchronizingSelectedItems) { try { _synchronizingSelectedItems = true; // Synchronize the SelectedItems collection foreach (object obj in removedDataItems.Select(di => di.Value)) { _selectedItems.Remove(obj); } foreach (object obj in addedDataItems.Select(di => di.Value)) { _selectedItems.Add(obj); } } finally { _synchronizingSelectedItems = false; } } // Pass the SelectionChanged event on to any listeners IList removedItems = removedDataItems.Select(di => di.Value).ToArray(); IList addedItems = addedDataItems.Select(di => di.Value).ToArray(); #if SILVERLIGHT SelectionChangedEventHandler handler = SelectionChanged; if (null != handler) { handler(this, new SelectionChangedEventArgs(removedItems, addedItems)); } #else RaiseEvent(new SelectionChangedEventArgs(SelectionChangedEvent, removedItems, addedItems)); #endif } /// /// Occurs when the selection of a DefinitionSeries changes. /// #if SILVERLIGHT public event SelectionChangedEventHandler SelectionChanged; #else public event SelectionChangedEventHandler SelectionChanged { add { AddHandler(SelectionChangedEvent, value); } remove { RemoveHandler(SelectionChangedEvent, value); } } /// /// Identifies the SelectionChanged routed event. /// public static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent("SelectionChanged", RoutingStrategy.Bubble, typeof(SelectionChangedEventHandler), typeof(DefinitionSeries)); #endif /// /// Builds the visual tree for the control when a new template is applied. /// public override void OnApplyTemplate() { if (null != _itemContainer) { _itemContainer.PrepareContainerForItem = null; _itemContainer.ClearContainerForItem = null; _itemContainer.ItemsSource = null; _itemContainer.Clip = null; _itemContainer.SizeChanged -= new SizeChangedEventHandler(ItemContainerSizeChanged); _itemContainer.SelectionChanged -= new SelectionChangedEventHandler(ItemContainerSelectionChanged); _itemContainer.ClearValue(Selector.SelectedIndexProperty); _itemContainer.ClearValue(Selector.SelectedItemProperty); } base.OnApplyTemplate(); SeriesArea = GetTemplateChild(SeriesAreaName) as Panel; if (null != SeriesArea) { _seriesAreaChildrenListAdapter.TargetList = SeriesArea.Children; _seriesAreaChildrenListAdapter.Populate(); } _itemContainer = GetTemplateChild(ItemContainerName) as DelegatingListBox; if (null != _itemContainer) { _itemContainer.PrepareContainerForItem = PrepareContainerForItem; _itemContainer.ClearContainerForItem = ClearContainerForItem; _itemContainer.ItemsSource = DataItems; _itemContainer.Clip = _clipGeometry; _itemContainer.SizeChanged += new SizeChangedEventHandler(ItemContainerSizeChanged); _itemContainer.SelectionChanged += new SelectionChangedEventHandler(ItemContainerSelectionChanged); _itemContainer.SetBinding(Selector.SelectedIndexProperty, new Binding("SelectedIndex") { Source = this, Mode = BindingMode.TwoWay }); _itemContainer.SetBinding(Selector.SelectedItemProperty, new Binding("SelectedItem") { Source = this, Mode = BindingMode.TwoWay, Converter = new SelectedItemToDataItemConverter(DataItems) }); } // Synchronize selection state with new ItemContainer OnSelectionModeChanged(SeriesSelectionMode.None, SelectionMode); SelectedItemsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } /// /// Prepares the specified element to display the specified item. /// /// The element used to display the specified item. /// The item to display. private void PrepareContainerForItem(DependencyObject element, object item) { DataItem dataItem = (DataItem)item; DataPoint dataPoint = CreateDataPoint(); dataItem.DataPoint = dataPoint; dataPoint.DataContext = dataItem.Value; dataPoint.SetBinding(DataPoint.DependentValueProperty, dataItem.SeriesDefinition.DependentValueBinding); dataPoint.SetBinding(DataPoint.IndependentValueProperty, dataItem.SeriesDefinition.IndependentValueBinding); dataPoint.SetBinding(DataPoint.StyleProperty, new Binding("ActualDataPointStyle") { Source = dataItem.SeriesDefinition }); dataPoint.DependentValueChanged += new RoutedPropertyChangedEventHandler(DataPointDependentValueChanged); dataPoint.ActualDependentValueChanged += new RoutedPropertyChangedEventHandler(DataPointActualDependentValueChanged); dataPoint.IndependentValueChanged += new RoutedPropertyChangedEventHandler(DataPointIndependentValueChanged); dataPoint.ActualIndependentValueChanged += new RoutedPropertyChangedEventHandler(DataPointActualIndependentValueChanged); dataPoint.StateChanged += new RoutedPropertyChangedEventHandler(DataPointStateChanged); dataPoint.DefinitionSeriesIsSelectionEnabledHandling = true; ContentControl container = (ContentControl)element; dataItem.Container = container; Binding selectionEnabledBinding = new Binding("SelectionMode") { Source = this, Converter = new SelectionModeToSelectionEnabledConverter() }; container.SetBinding(ContentControl.IsTabStopProperty, selectionEnabledBinding); dataPoint.SetBinding(DataPoint.IsSelectionEnabledProperty, selectionEnabledBinding); dataPoint.SetBinding(DataPoint.IsSelectedProperty, new Binding("IsSelected") { Source = container, Mode = BindingMode.TwoWay }); dataPoint.Visibility = Visibility.Collapsed; dataPoint.State = DataPointState.Showing; PrepareDataPoint(dataPoint); container.Content = dataPoint; } /// /// Undoes the effects of the PrepareContainerForItemOverride method. /// /// The container element. /// The item to display. private void ClearContainerForItem(DependencyObject element, object item) { DataItem dataItem = (DataItem)item; DataPoint dataPoint = dataItem.DataPoint; dataPoint.DependentValueChanged -= new RoutedPropertyChangedEventHandler(DataPointDependentValueChanged); dataPoint.ActualDependentValueChanged -= new RoutedPropertyChangedEventHandler(DataPointActualDependentValueChanged); dataPoint.IndependentValueChanged -= new RoutedPropertyChangedEventHandler(DataPointIndependentValueChanged); dataPoint.ActualIndependentValueChanged -= new RoutedPropertyChangedEventHandler(DataPointActualIndependentValueChanged); dataPoint.StateChanged -= new RoutedPropertyChangedEventHandler(DataPointStateChanged); dataPoint.ClearValue(DataPoint.DependentValueProperty); dataPoint.ClearValue(DataPoint.IndependentValueProperty); dataPoint.ClearValue(DataPoint.StyleProperty); dataPoint.ClearValue(DataPoint.IsSelectionEnabledProperty); dataPoint.ClearValue(DataPoint.IsSelectedProperty); ContentControl container = (ContentControl)dataItem.Container; container.ClearValue(ContentControl.IsTabStopProperty); dataPoint.DataContext = null; } /// /// Prepares a DataPoint for use. /// /// DataPoint instance. protected virtual void PrepareDataPoint(DataPoint dataPoint) { } /// /// Creates a DataPoint for the series. /// /// Series-appropriate DataPoint instance. protected abstract DataPoint CreateDataPoint(); /// /// Provides an internally-accessible wrapper for calling CreateDataPoint. /// /// Series-appropriate DataPoint instance. internal DataPoint InternalCreateDataPoint() { return CreateDataPoint(); } /// /// Handles the SizeChanged event of the ItemContainer. /// /// Event source. /// Event arguments. private void ItemContainerSizeChanged(object sender, SizeChangedEventArgs e) { _clipGeometry.Rect = new Rect(0, 0, e.NewSize.Width, e.NewSize.Height); QueueUpdateDataItemPlacement(false, false, DataItems); } /// /// Returns the DataItem corresponding to the specified DataPoint. /// /// Specified DataPoint. /// Corresponding DataItem. protected DataItem DataItemFromDataPoint(DataPoint dataPoint) { return DataItems.Where(di => di.DataPoint == dataPoint).Single(); } /// /// Handles the DependentValueChanged event of a DataPoint. /// /// Event source. /// Event arguments. private void DataPointDependentValueChanged(object sender, RoutedPropertyChangedEventArgs e) { DataPoint dataPoint = (DataPoint)sender; SeriesDefinition definition = DataItemFromDataPoint(dataPoint).SeriesDefinition; TimeSpan transitionDuration = definition.TransitionDuration; if (0 < transitionDuration.TotalMilliseconds) { dataPoint.BeginAnimation(DataPoint.ActualDependentValueProperty, "ActualDependentValue", e.NewValue, definition.TransitionDuration, definition.TransitionEasingFunction); } else { dataPoint.ActualDependentValue = e.NewValue; } } /// /// Handles the ActualDependentValueChanged event of a DataPoint. /// /// Event source. /// Event arguments. private void DataPointActualDependentValueChanged(object sender, RoutedPropertyChangedEventArgs e) { DataPoint dataPoint = (DataPoint)sender; QueueUpdateDataItemPlacement(true, false, DataItems.Where(di => di.DataPoint == dataPoint)); } /// /// Handles the IndependentValueChanged event of a DataPoint. /// /// Event source. /// Event arguments. private void DataPointIndependentValueChanged(object sender, RoutedPropertyChangedEventArgs e) { DataPoint dataPoint = (DataPoint)sender; SeriesDefinition definition = DataItemFromDataPoint(dataPoint).SeriesDefinition; TimeSpan transitionDuration = definition.TransitionDuration; if (0 < transitionDuration.TotalMilliseconds) { dataPoint.BeginAnimation(DataPoint.ActualIndependentValueProperty, "ActualIndependentValue", e.NewValue, definition.TransitionDuration, definition.TransitionEasingFunction); } else { dataPoint.ActualIndependentValue = e.NewValue; } } /// /// Handles the ActualIndependentValueChanged event of a DataPoint. /// /// Event source. /// Event arguments. private void DataPointActualIndependentValueChanged(object sender, RoutedPropertyChangedEventArgs e) { DataPoint dataPoint = (DataPoint)sender; QueueUpdateDataItemPlacement(false, true, DataItems.Where(di => di.DataPoint == dataPoint)); } /// /// Handles the StateChanged event of a DataPoint. /// /// Event source. /// Event arguments. private void DataPointStateChanged(object sender, RoutedPropertyChangedEventArgs e) { DataPoint dataPoint = (DataPoint)sender; if (DataPointState.Hidden == dataPoint.State) { DataItems.Remove(DataItems.Where(di => di.DataPoint == dataPoint).Single()); RemovedDataItems(); } } /// /// Notifies the specified axis of changes to values plotting against it. /// /// Specified axis. protected void NotifyAxisValuesChanged(IAxis axis) { if (null != axis) { IRangeConsumer rangeConsumer = axis as IRangeConsumer; if (null != rangeConsumer) { IRangeProvider rangeProvider = (IRangeProvider)this; rangeConsumer.RangeChanged(rangeProvider, new Range() /*rangeProvider.GetRange(rangeConsumer)*/); } IDataConsumer dataConsumer = axis as IDataConsumer; if (null != dataConsumer) { IDataProvider dataProvider = (IDataProvider)this; dataConsumer.DataChanged(dataProvider, null /*dataProvider.GetData(dataConsumer)*/); } } } /// /// Notifies the specified axis of changes to value margins plotting against it. /// /// Specified axis. /// Sequence of value margins that have changed. protected void NotifyValueMarginsChanged(IAxis axis, IEnumerable valueMargins) { if (null != axis) { IValueMarginConsumer valueMarginConsumer = axis as IValueMarginConsumer; if (null != valueMarginConsumer) { IValueMarginProvider valueMarginProvider = (IValueMarginProvider)this; valueMarginConsumer.ValueMarginsChanged(valueMarginProvider, valueMargins); } } } /// /// Handles the CollectionChanged event of the SeriesDefinitions collection. /// /// Event source. /// Event arguments. private void SeriesDefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { SeriesDefinitionsCollectionChanged(e.Action, e.OldItems, e.OldStartingIndex, e.NewItems, e.NewStartingIndex); } /// /// Handles the CollectionChanged event of the SeriesDefinitions collection. /// /// Type of change. /// Sequence of old items. /// Starting index of old items. /// Sequence of new items. /// Starting index of new items. protected virtual void SeriesDefinitionsCollectionChanged(NotifyCollectionChangedAction action, IList oldItems, int oldStartingIndex, IList newItems, int newStartingIndex) { if (null != oldItems) { foreach (SeriesDefinition oldDefinition in oldItems.CastWrapper()) { ISeries oldSeries = (ISeries)oldDefinition; SeriesDefinitionItemsSourceChanged(oldDefinition, oldDefinition.ItemsSource, null); _seriesDefinitionsAsISeries.Remove(oldDefinition); _legendItems.ChildCollections.Remove(oldSeries.LegendItems); UpdatePaletteProperties(oldDefinition); oldSeries.SeriesHost = null; oldDefinition.Index = -1; } } if (null != newItems) { int index = newStartingIndex; foreach (SeriesDefinition newDefinition in newItems.CastWrapper()) { ISeries newSeries = (ISeries)newDefinition; newSeries.SeriesHost = this; UpdatePaletteProperties(newDefinition); _legendItems.ChildCollections.Add(newSeries.LegendItems); _seriesDefinitionsAsISeries.Add(newDefinition); newDefinition.Index = index; SeriesDefinitionItemsSourceChanged(newDefinition, null, newDefinition.ItemsSource); index++; } } } /// /// Updates the palette properties of the specified SeriesDefinition. /// /// Specified SeriesDefinition. private void UpdatePaletteProperties(SeriesDefinition definition) { ResourceDictionary resources = null; if (null != SeriesHost) { Type dataPointType = CreateDataPoint().GetType(); using (IEnumerator enumerator = SeriesHost.GetResourceDictionariesWhere(dictionary => { Style style = dictionary["DataPointStyle"] as Style; if (null != style) { return (null != style.TargetType) && (style.TargetType.IsAssignableFrom(dataPointType)); } return false; })) { if (enumerator.MoveNext()) { resources = enumerator.Current; } } } definition.PaletteDataPointStyle = (null != resources) ? resources["DataPointStyle"] as Style : null; definition.PaletteDataShapeStyle = (null != resources) ? resources["DataShapeStyle"] as Style : null; definition.PaletteLegendItemStyle = (null != resources) ? resources["LegendItemStyle"] as Style : null; } /// /// Handles changes to the ItemsSource of a SeriesDefinition. /// /// SeriesDefinition owner. /// Old value. /// New value. internal void SeriesDefinitionItemsSourceChanged(SeriesDefinition definition, IEnumerable oldValue, IEnumerable newValue) { if (null != oldValue) { foreach (DataItem dataItem in DataItems.Where(di => di.SeriesDefinition == definition).ToArray()) { DataItems.Remove(dataItem); } RemovedDataItems(); } if (null != newValue) { // No need to add items if SeriesHost null; setting SeriesHost will take care of that if (null != SeriesHost) { AddDataItems(definition, newValue.CastWrapper(), 0); } } } /// /// Handles changes to the ItemsSource collection of a SeriesDefinition. /// /// SeriesDefinition owner. /// Type of change. /// Sequence of old items. /// Starting index of old items. /// Sequence of new items. /// Starting index of new items. [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Linq is artificially increasing the rating.")] internal void SeriesDefinitionItemsSourceCollectionChanged(SeriesDefinition definition, NotifyCollectionChangedAction action, IList oldItems, int oldStartingIndex, IList newItems, int newStartingIndex) { if (NotifyCollectionChangedAction.Replace == action) { // Perform in-place replacements foreach (DataItem dataItem in DataItems.Where(di => (di.SeriesDefinition == definition) && (newStartingIndex <= di.Index) && (di.Index < newStartingIndex + newItems.Count))) { dataItem.Value = newItems[dataItem.Index - newStartingIndex]; } } else { if (NotifyCollectionChangedAction.Reset == action) { // Set up parameters to allow normal old/new item handling to be used Debug.Assert(null == oldItems, "Reset action with non-null oldItems."); oldItems = DataItems.Where(di => (di.SeriesDefinition == definition)).ToArray(); oldStartingIndex = 0; newItems = definition.ItemsSource.CastWrapper().ToArray(); newStartingIndex = 0; } if (null != oldItems) { // Get rid of old items foreach (DataItem oldDataItem in DataItems.Where(di => (di.SeriesDefinition == definition) && (oldStartingIndex <= di.Index) && (di.Index < oldStartingIndex + oldItems.Count))) { oldDataItem.Index = -1; if (null != oldDataItem.DataPoint) { oldDataItem.DataPoint.State = DataPointState.Hiding; } } // Adjust index of shifted items foreach (DataItem dataItem in DataItems.Where(di => (di.SeriesDefinition == definition) && (oldStartingIndex + oldItems.Count <= di.Index))) { dataItem.Index -= oldItems.Count; } } if (null != newItems) { // Adjust index of shifted items foreach (DataItem dataItem in DataItems.Where(di => (di.SeriesDefinition == definition) && (newStartingIndex <= di.Index))) { dataItem.Index += newItems.Count; } // Add new items AddDataItems(definition, newItems.CastWrapper(), newStartingIndex); } } #if DEBUG // Validate all DataItem index and value properties foreach (var group in DataItems.Where(di => 0 <= di.Index).OrderBy(di => di.Index).GroupBy(di => di.SeriesDefinition)) { object[] items = group.Key.ItemsSource.CastWrapper().ToArray(); int i = 0; foreach (DataItem dataItem in group) { Debug.Assert(i == dataItem.Index, "DataItem index mis-match."); Debug.Assert(dataItem.Value.Equals(items[i]), "DataItem value mis-match."); i++; } } #endif } /// /// Handles the ResourceDictionariesChanged event of the SeriesHost owner. /// /// Event source. /// Event arguments. private void SeriesHostResourceDictionariesChanged(object sender, EventArgs e) { foreach (SeriesDefinition definition in SeriesDefinitions) { UpdatePaletteProperties(definition); } } /// /// Creates and adds DataItems for the specified SeriesDefinition's items. /// /// Specified SeriesDefinition. /// Sequence of items. /// Starting index. private void AddDataItems(SeriesDefinition definition, IEnumerable items, int startingIndex) { int index = startingIndex; foreach (object item in items) { DataItems.Add(new DataItem(definition) { Value = item, Index = index }); index++; } // Because properties (like DependentValueBinding) may still be getting set Dispatcher.BeginInvoke((Action)AddedDataItems); } /// /// Updates the axes after DataItems have been added. /// private void AddedDataItems() { EnsureAxes(false, false, true); } /// /// Notifies the axes after DataItems have been removed. /// private void RemovedDataItems() { NotifyAxisValuesChanged(ActualIndependentAxis); NotifyAxisValuesChanged(ActualDependentAxis); } /// /// Ensures that suitable axes are present and registered. /// /// True if the dependent axis needs to be updated. /// True if the independent axis needs to be updated. /// True if both axis are to be notified unconditionally. private void EnsureAxes(bool updateDependentAxis, bool updateIndependentAxis, bool unconditionallyNotifyAxes) { foreach (SeriesDefinition definition in SeriesDefinitions) { if (null == definition.DependentValueBinding) { throw new InvalidOperationException(Properties.Resources.DefinitionSeries_EnsureAxes_MissingDependentValueBinding); } if (null == definition.IndependentValueBinding) { throw new InvalidOperationException(Properties.Resources.DefinitionSeries_EnsureAxes_MissingIndependentValueBinding); } } if ((null != SeriesHost) && DataItems.Any()) { // Ensure a dependent axis is present or updated bool changedActualDependentAxis = false; if (updateDependentAxis && (null != ActualDependentAxis)) { ActualDependentAxis.RegisteredListeners.Remove(this); ActualDependentAxis = null; } if (null == ActualDependentAxis) { ActualDependentAxis = DependentAxis ?? AcquireDependentAxis(); ActualDependentAxis.RegisteredListeners.Add(this); if (!SeriesHost.Axes.Contains(ActualDependentAxis)) { SeriesHost.Axes.Add(ActualDependentAxis); } changedActualDependentAxis = true; } // Ensure an independent axis is present or updated bool changedActualIndependentAxis = false; if (updateIndependentAxis && (null != ActualIndependentAxis)) { ActualIndependentAxis.RegisteredListeners.Remove(this); ActualIndependentAxis = null; } if (null == ActualIndependentAxis) { ActualIndependentAxis = IndependentAxis ?? AcquireIndependentAxis(); ActualIndependentAxis.RegisteredListeners.Add(this); if (!SeriesHost.Axes.Contains(ActualIndependentAxis)) { SeriesHost.Axes.Add(ActualIndependentAxis); } changedActualIndependentAxis = true; } // Queue an update if necessary or requested if (changedActualDependentAxis || changedActualIndependentAxis || unconditionallyNotifyAxes) { QueueUpdateDataItemPlacement(changedActualDependentAxis || unconditionallyNotifyAxes, changedActualIndependentAxis || unconditionallyNotifyAxes, DataItems); } } } /// /// Acquires a dependent axis suitable for use with the data values of the series. /// /// Axis instance. protected abstract IAxis AcquireDependentAxis(); /// /// Acquires an independent axis suitable for use with the data values of the series. /// /// Axis instance. protected abstract IAxis AcquireIndependentAxis(); /// /// Handles notification of the invalidation of an axis. /// /// Invalidated axis. void IAxisListener.AxisInvalidated(IAxis axis) { QueueUpdateDataItemPlacement(false, false, DataItems); } /// /// Queues an update of DataItem placement for the next update opportunity. /// /// True if the dependent axis values have changed. /// True if the independent axis values have changed. /// Sequence of DataItems to update. private void QueueUpdateDataItemPlacement(bool dependentAxisValuesChanged, bool independentAxisValuesChanged, IEnumerable dataItems) { _queueUpdateDataItemPlacement_DependentAxisValuesChanged |= dependentAxisValuesChanged; _queueUpdateDataItemPlacement_IndependentAxisValuesChanged |= independentAxisValuesChanged; _queueUpdateDataItemPlacement_DataItems.AddRange(dataItems); InvalidateArrange(); } /// /// Called when the control needs to arrange its children. /// /// Bounds to arrange within. /// Arranged size. /// /// Used as a good place to dequeue queued work. /// protected override Size ArrangeOverride(Size arrangeBounds) { Size arrangedSize = base.ArrangeOverride(arrangeBounds); if (_queueUpdateDataItemPlacement_DependentAxisValuesChanged) { NotifyAxisValuesChanged(ActualDependentAxis); _queueUpdateDataItemPlacement_DependentAxisValuesChanged = false; } if (_queueUpdateDataItemPlacement_IndependentAxisValuesChanged) { NotifyAxisValuesChanged(ActualIndependentAxis); _queueUpdateDataItemPlacement_IndependentAxisValuesChanged = false; } UpdateDataItemPlacement(_queueUpdateDataItemPlacement_DataItems.Distinct()); _queueUpdateDataItemPlacement_DataItems.Clear(); return arrangedSize; } /// /// Updates the placement of the DataItems (data points) of the series. /// /// DataItems in need of an update. protected abstract void UpdateDataItemPlacement(IEnumerable dataItems); /// /// Returns the range for the data points of the series. /// /// Consumer of the range. /// Range of values. Range IRangeProvider.GetRange(IRangeConsumer rangeConsumer) { return IRangeProviderGetRange(rangeConsumer); } /// /// Returns the range for the data points of the series. /// /// Consumer of the range. /// Range of values. protected virtual Range IRangeProviderGetRange(IRangeConsumer rangeConsumer) { if (rangeConsumer == ActualIndependentAxis) { if (ActualIndependentAxis.CanPlot(0.0)) { return IndependentValueGroups .Select(g => ValueHelper.ToDouble(g.IndependentValue)) .Where(d => ValueHelper.CanGraph(d)) .DefaultIfEmpty() .CastWrapper() .GetRange(); } else { return IndependentValueGroups .Select(g => ValueHelper.ToDateTime(g.IndependentValue)) .DefaultIfEmpty() .CastWrapper() .GetRange(); } } throw new NotSupportedException(); } /// /// Returns the value margins for the data points of the series. /// /// Consumer of the value margins. /// Sequence of value margins. IEnumerable IValueMarginProvider.GetValueMargins(IValueMarginConsumer valueMarginConsumer) { return IValueMarginProviderGetValueMargins(valueMarginConsumer); } /// /// Returns the value margins for the data points of the series. /// /// Consumer of the value margins. /// Sequence of value margins. protected virtual IEnumerable IValueMarginProviderGetValueMargins(IValueMarginConsumer valueMarginConsumer) { throw new NotImplementedException(); } /// /// Returns the data for the data points of the series. /// /// Consumer of the data. /// Sequence of data. IEnumerable IDataProvider.GetData(IDataConsumer dataConsumer) { return IDataProviderGetData(dataConsumer); } /// /// Returns the data for the data points of the series. /// /// Consumer of the data. /// Sequence of data. protected virtual IEnumerable IDataProviderGetData(IDataConsumer dataConsumer) { if (dataConsumer == ActualIndependentAxis) { return IndependentValueGroups.Select(cg => cg.IndependentValue).Distinct(); } throw new NotImplementedException(); } /// /// Gets a sequence of IndependentValueGroups. /// protected virtual IEnumerable IndependentValueGroups { get { return DataItems .GroupBy(di => di.ActualIndependentValue) .Select(g => new IndependentValueGroup(g.Key, g.OrderBy(di => di.SeriesDefinition.Index))); } } /// /// Gets a sequence of IndependentValueGroups ordered by independent value. /// protected IEnumerable IndependentValueGroupsOrderedByIndependentValue { get { return IndependentValueGroups .OrderBy(g => g.IndependentValue); } } /// /// Gets a sequence of sequences of the dependent values associated with each independent value. /// [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Nesting reflects the actual hierarchy of the data.")] protected IEnumerable> IndependentValueDependentValues { get { return IndependentValueGroups .Select(g => { g.Denominator = IsStacked100 ? g.DataItems.Sum(di => Math.Abs(ValueHelper.ToDouble(di.ActualDependentValue))) : 1; if (0 == g.Denominator) { g.Denominator = 1; } return g; }) .Select(g => g.DataItems .Select(di => ValueHelper.ToDouble(di.ActualDependentValue) * (IsStacked100 ? (100 / g.Denominator) : 1))); } } /// /// Represents an independent value and the dependent values that are associated with it. /// protected class IndependentValueGroup { /// /// Initializes a new instance of the IndependentValueGroup class. /// /// Independent value. /// Associated DataItems. public IndependentValueGroup(object independentValue, IEnumerable dataItems) { IndependentValue = independentValue; DataItems = dataItems; } /// /// Gets the independent value. /// public object IndependentValue { get; private set; } /// /// Gets a sequence of DataItems associated with the independent value. /// public IEnumerable DataItems { get; private set; } /// /// Gets or sets the denominator to use when computing with this instance. /// /// /// Exists here purely to simplify the the corresponding algorithm. /// public double Denominator { get; set; } } /// /// Represents a single data value from a SeriesDefinition's ItemsSource. /// protected class DataItem { /// /// Stores a reference to a shared BindingHelper instance. /// private static readonly BindingHelper _bindingHelper = new BindingHelper(); /// /// Initializes a new instance of the DataItem class. /// /// SeriesDefinition owner. public DataItem(SeriesDefinition seriesDefinition) { SeriesDefinition = seriesDefinition; CenterPoint = new Point(double.NaN, double.NaN); } /// /// Gets the SeriesDefinition owner of the DataItem. /// public SeriesDefinition SeriesDefinition { get; private set; } /// /// Gets or sets the value of the DataItem. /// public object Value { get { return _value; } set { _value = value; if (null != DataPoint) { DataPoint.DataContext = value; } } } /// /// Stores the value of the DataItem. /// private object _value; /// /// Gets or sets the index of the DataItem. /// public int Index { get; set; } /// /// Gets or sets the DataPoint associated with the DataItem. /// public DataPoint DataPoint { get; set; } /// /// Gets or sets the container for the DataPoint within its parent ItemsControl. /// public UIElement Container { get; set; } /// /// Gets the ActualDependentValue of the DataPoint (or its equivalent). /// public IComparable ActualDependentValue { get { if (null != DataPoint) { return DataPoint.ActualDependentValue; } else { return (IComparable)_bindingHelper.EvaluateBinding(SeriesDefinition.DependentValueBinding, Value); } } } /// /// Gets the ActualIndependentValue of the DataPoint (or its equivalent). /// public object ActualIndependentValue { get { if (null != DataPoint) { return DataPoint.ActualIndependentValue; } else { return _bindingHelper.EvaluateBinding(SeriesDefinition.IndependentValueBinding, Value); } } } /// /// Gets or sets the ActualDependentValue of the DataPoint after adjusting for applicable stacking. /// public double ActualStackedDependentValue { get; set; } /// /// Gets or sets the center-point of the DataPoint in plot area coordinates (if relevant). /// public Point CenterPoint { get; set; } } /// /// Provides an easy way to evaluate a Binding against a source instance. /// private class BindingHelper : FrameworkElement { /// /// Initializes a new instance of the BindingHelper class. /// public BindingHelper() { } /// /// Identifies the Result dependency property. /// private static readonly DependencyProperty ResultProperty = DependencyProperty.Register("Result", typeof(object), typeof(BindingHelper), null); /// /// Evaluates a Binding against a source instance. /// /// Binding to evaluate. /// Source instance. /// Result of Binding on source instance. public object EvaluateBinding(Binding binding, object instance) { DataContext = instance; SetBinding(ResultProperty, binding); object result = GetValue(ResultProperty); ClearValue(ResultProperty); DataContext = null; return result; } } /// /// Converts from a selected item to the corresponding DataItem. /// private class SelectedItemToDataItemConverter : IValueConverter { /// /// Stores a reference to the DataItem collection. /// private ObservableCollection _dataItems; /// /// Initializes a new instance of the SelectedItemToDataItemConverter class. /// /// Collection of DataItems. public SelectedItemToDataItemConverter(ObservableCollection dataItems) { _dataItems = dataItems; } /// /// Converts a value. /// /// The value produced by the binding source. /// The type of the binding target property. /// The converter parameter to use. /// The culture to use in the converter. /// Converted value. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return _dataItems.Where(di => di.Value == value).FirstOrDefault(); } /// /// Converts a value back. /// /// The value produced by the binding source. /// The type of the binding target property. /// The converter parameter to use. /// The culture to use in the converter. /// Converted value. public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { DataItem dataItem = value as DataItem; return (null != dataItem) ? dataItem.Value : null; } } /// /// Converts from a SeriesSelectionMode to a true/false value indicating whether selection is enabled. /// private class SelectionModeToSelectionEnabledConverter : IValueConverter { /// /// Initializes a new instance of the SelectionModeToSelectionEnabledConverter class. /// public SelectionModeToSelectionEnabledConverter() { } /// /// Converts a value. /// /// The value produced by the binding source. /// The type of the binding target property. /// The converter parameter to use. /// The culture to use in the converter. /// Converted value. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { bool isSelectionEnabled = false; if (value is SeriesSelectionMode) { isSelectionEnabled = !(SeriesSelectionMode.None == (SeriesSelectionMode)value); } return isSelectionEnabled; } /// /// Converts a value back. /// /// The value produced by the binding source. /// The type of the binding target property. /// The converter parameter to use. /// The culture to use in the converter. /// Converted value. public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } /// /// Gets the axes for the series as a series host. /// [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Justification = "Property exists as an interface requirement; implementation is unnecessary.")] ObservableCollection ISeriesHost.Axes { get { throw new NotImplementedException(); } } /// /// Gets the series for the series as a series host. /// ObservableCollection ISeriesHost.Series { get { return _seriesDefinitionsAsISeries; } } /// /// Gets the foreground elements for the series as a series host. /// [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Justification = "Property exists as an interface requirement; implementation is unnecessary.")] ObservableCollection ISeriesHost.ForegroundElements { get { throw new NotImplementedException(); } } /// /// Gets the background elements for the series as a series host. /// [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Justification = "Property exists as an interface requirement; implementation is unnecessary.")] ObservableCollection ISeriesHost.BackgroundElements { get { throw new NotImplementedException(); } } /// /// Gets a IResourceDictionaryDispenser for the series as a series host. /// /// Predicate function. /// Sequence of ResourceDictionaries. IEnumerator IResourceDictionaryDispenser.GetResourceDictionariesWhere(Func predicate) { throw new NotImplementedException(); } /// /// Event that is triggered when the available ResourceDictionaries change. /// [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Justification = "Property exists as an interface requirement; implementation is unnecessary.")] event EventHandler IResourceDictionaryDispenser.ResourceDictionariesChanged { add { throw new NotImplementedException(); } remove { throw new NotImplementedException(); } } } }