Silverlight ChartHelper revisited...
(Cross-posed from my old, personal blog – Originally published 6/10/2009)
For my current project, I'm integrating the Silverlight Toolkit's charting functionality with our application. As part of that effort, I came across this excellent post by Jeremiah Morrill on binding multiple chart series, where he provided the code to an attached behavior that supports this scenario. However, I had a few additional requirements that his original didn't handle. Most notably, we needed the ability to set the X and Y axis titles and to set the minimum and maximum values on each axis. Although I'm not going to copy all of the code here, there were a few interesting challenges in implementing these changes that I'd like to discuss. The complete source code for my updated example is available for download. (Updated to rename the tile .txt so it is downloadable)
The Silverlight toolkit's charting module is incredibly modularized, being broken up into axes, series, etc. object hierarchies which together display the chart data. This allows for a great deal of flexibility, but also makes for some "hoop-jumping" to figure out when in the lifecycle of the chart/series/axes you need to set certain items.
When you are creating your series manually via XAML, it is obvious that these options are easily available. However, when utilizing the ChartHelper, there was no obvious way to get access to the axes when adding the series, as the chart selects the axes based on the series type you add.
// NOTE: In order to change the Title, Maximum, and Minimum properties of the axes, you must handle the Axes_CollectionChanged event.
// However, you only want this event to fire once, so we always remove our handler here, and then re-add it to make sure we don't have
// an ever-increasing number of calls to Axes_CollectionChanged
((ISeriesHost)chart).Axes.CollectionChanged -= new NotifyCollectionChangedEventHandler(Axes_CollectionChanged);
((ISeriesHost)chart).Axes.CollectionChanged +=new NotifyCollectionChangedEventHandler(Axes_CollectionChanged);
}
/// <summary>
/// Handles the CollectionChanged event of the Axes control. Here, X and Y axis titles and min/max properties are set once the graph creates or assigns the axes we need.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="ccEventArgs">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
static void Axes_CollectionChanged(object sender, NotifyCollectionChangedEventArgs ccEventArgs)
{
if (ccEventArgs.Action != NotifyCollectionChangedAction.Remove)
{
Chart chart = null;
foreach (DisplayAxis axis in ccEventArgs.NewItems)
{
chart = (Chart)axis.SeriesHost;
if ((axis.Orientation == AxisOrientation.X && GetSeriesType(chart)!=SeriesType.Bar) || (axis.Orientation == AxisOrientation.Y && GetSeriesType(chart)==SeriesType.Bar))
{
axis.SetBinding(DisplayAxis.TitleProperty, new Binding(GetXAxisTitle(chart)));
if (axis is LinearAxis)
{
axis.SetBinding(LinearAxis.MinimumProperty, new Binding(GetXAxisMinimum(chart)));
axis.SetBinding(LinearAxis.MaximumProperty, new Binding(GetXAxisMaximum(chart)));
}
else if (axis is DateTimeAxis)
{
axis.SetBinding(DateTimeAxis.MinimumProperty, new Binding(GetXAxisMinimum(chart)));
axis.SetBinding(DateTimeAxis.MaximumProperty, new Binding(GetXAxisMaximum(chart)));
}
}
else
{
axis.SetBinding(DisplayAxis.TitleProperty, new Binding(GetYAxisTitle(chart)));
if (axis is LinearAxis)
{
axis.SetBinding(LinearAxis.MinimumProperty, new Binding(GetYAxisMinimum(chart)));
axis.SetBinding(LinearAxis.MaximumProperty, new Binding(GetYAxisMaximum(chart)));
}
else if (axis is DateTimeAxis)
{
axis.SetBinding(DateTimeAxis.MinimumProperty, new Binding(GetYAxisMinimum(chart)));
axis.SetBinding(DateTimeAxis.MaximumProperty, new Binding(GetYAxisMaximum(chart)));
}
}
}
}
}
The trick (at least as far as I understand so far) is to attach a handler to the chart's Axes collection's CollectionChanged event and, in that event, apply the appropriate bindings to the axes. So, at the end of Jeremiah's SeriesSourceChanged event, I get a reference to the chart's axes collection and add the event handler. Note that I first remove, and then re-add the event handler. The reason for this is that there's no other way I could find to remove the previous delegate before adding the new one when the series source had multiple changes (which it often does in my case). The relevant code is below:
Note that I've handled Linear and DateTime axes so far - this handles the situations I've run into at this point, but it may need to be extended. Also note that the only reason I've got these if statements is because the minimum and maximum dependency properties are defined separately on each axis type, which is something of a pain. perhaps DateTime and Linear axes could at some point derive from a common base where the Min and Max properties are defined, so this kind of code would be unnecessary.
The other trick is that, for Bar-type charts, you really need to treat X and Y axes opposite of every other chart type, as the bar type switches the dependent and independent axes.
I hope this is helpful to someone out there, and thanks again to Jeremiah (Jer?) for the initial implementation.
P.S. - I apologize for the long line lengths - I have 1920x1200 monitors everywhere and have a tendency to let my code get wider than it should be, which I've now seen quite dramatically in this post.
Doug