This post describes a method of using attached properties to bind a ViewModel which contains multiple data series to a Visiblox chart without any code-behind.
The Visiblox chart supports databinding in both WPF and Silverlight, where the X and Y values for each datapoint are bound to properties on an underlying model. However, there is no interface for binding a varying number of series (i.e., a collection of collections). The solution provided here is similar to the one which Jeremiah Morrill published for binding multiple series to the Silverlight Toolkit charts, but with a few added extras, like series title binding and series type selection.
The solution is surprisingly simple, so I am going to dive straight into the code (note, I have collapsed the verbose attached property definitions so that just the interesting bits are shown below!)
public static class MultiSeries
{
#region TitlePath attached property
#region ItemsSourcePath attached property
#region XValuePath attached property
#region YValuePath attached property
#region ChartTypeProvider attached property
#region Source attached property
private static void OnSourcePropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
Chart targetChart = d as Chart;
SynchroniseChartWithSource(targetChart);
IEnumerable Source = GetSource(targetChart);
INotifyCollectionChanged incc = Source as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += (s, e2) => SynchroniseChartWithSource(targetChart);
}
}
private static void SynchroniseChartWithSource(Chart chart)
{
chart.Series.Clear();
IEnumerable Source = GetSource(chart);
if (Source == null)
return;
foreach (object seriesDataSource in Source)
{
IChartSeries chartSeries = GetChartTypeProvider(chart).GetSeries(seriesDataSource);
var itemsSourcePath = GetItemsSourcePath(chart);
IEnumerable itemsSource = null;
if (!string.IsNullOrEmpty(itemsSourcePath))
{
itemsSource = seriesDataSource.GetPropertyValue<IEnumerable>(itemsSourcePath);
}
else
{
itemsSource = seriesDataSource as IEnumerable;
}
var titlePath = GetTitlePath(chart);
var seriesTitle = "";
if (!string.IsNullOrEmpty(titlePath))
{
seriesTitle = seriesDataSource.GetPropertyValue<string>(titlePath);
}
chartSeries.DataSeries = new BindableDataSeries()
{
XValueBinding = new Binding(GetXValuePath(chart)),
YValueBinding = new Binding(GetYValuePath(chart)),
ItemsSource = itemsSource,
Title = seriesTitle
};
chart.Series.Add(chartSeries);
}
}
public static T GetPropertyValue<T>(this object source, string propertyName)
{
var property = source.GetType().GetProperty(propertyName);
if (property == null)
{
throw new ArgumentException(string.Format("The property {0} does not exist on the type {1}",
propertyName, source.GetType()));
}
return (T)property.GetValue(source, null);
}
}
The MultiSeries
class defines a number of attached properties, Source
is used to bind the collection of series, this property must be an IEnumerable
, but if it also implements INotifyCollectionChanged
, we handle the CollectionChanged
events to update the chart (adding or removing series). The optional ItemsSourcePath
is used to provide the path to the nested collection (more on this later) and the optional TitlePath
binds the chart title. The XValuePath
and YValuePath
properties are used to bind the X & Y values of the chart. Finally, ChartTypeProvider
is used to determine the series type (Line
, Bar
, Column
…) for each of the bound series. The provider must implement the following interface:
public interface IChartTypeProvider
{
IChartSeries GetSeries(object boundObject);
}
In most MVVM chart binding applications, you will probably want all the series to have the same type. To achieve this, we can create a simple implementation of this interface
which always returns the same chart type:
public class DefaultChartTypeProvider : IChartTypeProvider
{
private Type _seriesType;
public DefaultChartTypeProvider(Type seriesType)
{
_seriesType = seriesType;
}
public IChartSeries GetSeries(object boundObject)
{
var ctr = _seriesType.GetConstructor(new Type[] { });
return (IChartSeries)ctr.Invoke(new object[] { });
}
}
In order to simplify the usage of this provider in XAML, we can provide a type converter which allows us to specify the required series type as a string
, e.g., ChartTypeProvider="LineSeries"
, this makes use of the same framework mechanisms that allow you to specify a Fill
as a string, e.g. Fill="Red"
, where the result will be to create the following, new SolidColorBrush() { Color = Colors.Red }
, a suitable type converter is shown below:
[TypeConverter(typeof(StringToChartTypeProvider))]
public interface IChartTypeProvider
{
}
public class StringToChartTypeProvider : TypeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
if (value is string)
{
var visibloxAssembly = typeof(Chart).Assembly;
var seriesType = visibloxAssembly.GetType("Visiblox.Charts." + value.ToString());
return new DefaultChartTypeProvider(seriesType);
}
return base.ConvertFrom(context, culture, value);
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
}
A Simple Example
We’ll start with a simple example, binding a collection of collections, in this case the harmonic vibrations of a string. The series are created as follows:
public MainPage()
{
InitializeComponent();
var harmonics = new List<List<Point>>();
for (int frequency = 1; frequency < 5; frequency++)
{
var upperHarmonic = new List<Point>();
var lowerHarmonic = new List<Point>();
for (double phase = 0; phase < Math.PI; phase += (Math.PI / 100))
{
upperHarmonic.Add(new Point(phase, Math.Sin(phase * frequency) + frequency * 2.5));
lowerHarmonic.Add(new Point(phase, Math.Sin(phase * frequency + Math.PI) + frequency * 2.5));
}
harmonics.Add(upperHarmonic);
harmonics.Add(lowerHarmonic);
}
this.DataContext = harmonics;
}
We can then use the attached properties defined above to bind this data to the chart. Note, because the bound data is a collection of collections (rather than a collection of items which expose the child collection via a property), the optional ItemsSourcePath
is not required:
<UserControl.Resources>
<vis:Palette x:Key="palette">
<Style TargetType="vis:LineSeries">
<Setter Property="LineStroke" Value="Black"/>
</Style>
<Style TargetType="vis:LineSeries">
<Setter Property="LineStroke" Value="Black"/>
</Style>
<Style TargetType="vis:LineSeries">
<Setter Property="LineStroke" Value="Red"/>
</Style>
<Style TargetType="vis:LineSeries">
<Setter Property="LineStroke" Value="Red"/>
</Style>
</vis:Palette>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<vis:Chart x:Name="chart"
Palette="{StaticResource palette}"
LegendVisibility="Collapsed"
Title="Modes of a vibrating string"
local:MultiSeries.ChartTypeProvider="LineSeries"
local:MultiSeries.XValuePath="X"
local:MultiSeries.YValuePath="Y"
local:MultiSeries.Source="{Binding}"/>
</Grid>
Whilst the MultiSeries
class allows you to specify the chart type via ChartTypeProvider
, it does not provide a mechanism for styling the various series which it produces based on the supplied Source
. However, styling can be achieved using the chart’s Palette
, which is a collection of Style
instances which are applied to the series in order.
The resulting chart is shown below:
A More Complex MVVM Example
In this example, rather than binding a collection of collections, the following model is bound:
CompanySalesViewModel
has a collection of SalesTeamViewModel
instances, each of these has a name, TeamName
, and a collection of SalesInRegionViewModel
instances. Note, SalesTeamViewModel
also has a string
indexer property which will be used to bind this model to a DataGrid
.
A custom ChartTypeProvider
is used so that we can select chart types based on the bound SalesTeamViewModel
:
public class SalesTypeProvider : IChartTypeProvider
{
public IChartSeries GetSeries(object boundObject)
{
var viewModel = boundObject as SalesTeamViewModel;
if (viewModel.TeamName == "Target")
return new LineSeries();
else
return new ColumnSeries();
}
}
The view model is bound to a chart and is also bound to a DataGrid
as follows. Note this time the ItemsSourcePath
is used to bind the TeamSales
property of each SalesTeamViewModel
:
<UserControl.Resources>
<DropShadowEffect x:Key="shadow" Opacity="0.3"/>
<Style TargetType="UIElement" x:Key="collapsedStyle">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<Style TargetType="vis:ColumnSeries" x:Key="columnBaseStyle">
<Setter Property="PointStroke" Value="White"/>
<Setter Property="PointStrokeThickness" Value="0.5"/>
<Setter Property="Effect" Value="{StaticResource shadow}"/>
</Style>
<vis:Palette x:Key="palette">
<Style TargetType="vis:ColumnSeries"
BasedOn="{StaticResource columnBaseStyle}">
<Setter Property="PointFill" Value="#4f81bd"/>
</Style>
<Style TargetType="vis:ColumnSeries"
BasedOn="{StaticResource columnBaseStyle}">
<Setter Property="PointFill" Value="#c0504d"/>
</Style>
<Style TargetType="vis:ColumnSeries"
BasedOn="{StaticResource columnBaseStyle}">
<Setter Property="PointFill" Value="#9bbb59"/>
</Style>
<Style TargetType="vis:LineSeries">
<Setter Property="LineStroke" Value="#8064a2"/>
<Setter Property="LineStrokeThickness" Value="4"/>
<Setter Property="Effect" Value="{StaticResource shadow}"/>
</Style>
</vis:Palette>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White"
util:GridUtils.RowDefinitions=",Auto">
<vis:Chart x:Name="chart"
Title="Global Team Sales"
LegendTitle="Team"
Palette="{StaticResource palette}"
local:MultiSeries.XValuePath="Region"
local:MultiSeries.YValuePath="Sales"
local:MultiSeries.TitlePath="TeamName"
local:MultiSeries.ItemsSourcePath="TeamSales"
local:MultiSeries.Source="{Binding SalesTeams}">
<local:MultiSeries.ChartTypeProvider>
<local:SalesTypeProvider/>
</local:MultiSeries.ChartTypeProvider>
<vis:Chart.XAxis>
<vis:CategoryAxis GridlineStyle="{StaticResource collapsedStyle}" />
</vis:Chart.XAxis>
</vis:Chart>
<sdk:DataGrid Grid.Row="1"
ItemsSource="{Binding SalesTeams}"
AutoGenerateColumns="False">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Binding="{Binding TeamName}"
Header="Team"
IsReadOnly="True"/>
<sdk:DataGridTextColumn Binding="{Binding [US]}"
Header="US"/>
<sdk:DataGridTextColumn Binding="{Binding [UK]}"
Header="UK"/>
<sdk:DataGridTextColumn Binding="{Binding [Germany]}"
Header="Germany"/>
<sdk:DataGridTextColumn Binding="{Binding [Japan]}"
Header="Japan"/>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
</Grid>
The result is shown below, note that updating the data in the grid causes the chart to update accordingly:
Preview (see original blog entry for operational Silverlight version)
You can download the full source code for this example here.
Regards,
Colin E.