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