Introduction
It is well known that the Microsoft (MS) Chart control was specifically developed for Windows Forms and ASP.NET applications. The control suite offers a wide array of chart types and charting features, including all of the standard chart types -- line charts, bar charts, pie charts, and so forth -- as well as more specialized charts, like pyramid, bubble, stock, and technical indicator charts. It also provides a comprehensive set of charting features, including support for multiple series, customizable legends, trend lines, and labels.
Unfortunately, the MS chart control does not directly support WPF and MVVM. You have to use the WindowsFormsHost
element to host the MS chart control if you really want to use it in your WPF applications, which will break the WPF data binding and MVVM rule. You might notice that Microsoft released a WPF Toolkit charting control few years ago. However, this toolkit has limited chart type support and runs very slow. There is not much you can do to improve its performance.
In this article, I will show you how to encapsulate the MS chart controls into a WPF UserControl
, which will be MVVM compatible. You can then use this WPF control in your WPF applications with MVVM pattern and data binding in the same way as you will do for the WPF built-in controls.
WPF Compatible MS Chart Control
Here, I will embed the original MS chart control into a WPF UserControl
named MsChart
using the WindowsFormsHost
element:
<UserControl x:Class="WpfMsChart.MsChart"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mschart="clr-namespace:System.Windows.Forms.DataVisualization.Charting;
assembly=System.Windows.Forms.DataVisualization"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="grid1" Margin="10">
<WindowsFormsHost Background="{x:Null}">
<mschart:Chart x:Name="myChart"/>
</WindowsFormsHost>
</Grid>
</UserControl>
This XAML is very simple. First, we need to map the .NET namespace and assembly of the MS chart control to an XML namespace: System.Windows.Forms.DataVisualization
. Using this XML namespace and the MS chart control class name (i.e., Chart
), we add the chart control to a WindowsFormsHost
and name it myChart
.
Since the original MS chart control does not support WPF data binding and the MVVM pattern, we will use the code-behind code to implement this WPF MsChart UserControl
. The following code snippet is our implementation for this control:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms.DataVisualization.Charting;
using Caliburn.Micro;
using System.Collections.Specialized;
namespace WpfMsChart
{
public partial class MsChart : UserControl
{
public MsChart()
{
InitializeComponent();
SeriesCollection = new BindableCollection<Series>();
}
public static DependencyProperty XValueTypeProperty =
DependencyProperty.Register("XValueType", typeof(string),
typeof(MsChart), new FrameworkPropertyMetadata
("Double", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public string XValueType
{
get { return (string)GetValue(XValueTypeProperty); }
set { SetValue(XValueTypeProperty, value); }
}
public static DependencyProperty XLabelProperty =
DependencyProperty.Register("XLabel", typeof(string),
typeof(MsChart), new FrameworkPropertyMetadata("X Axis",
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
......
public BindableCollection<Series> SeriesCollection
{
get { return (BindableCollection<Series>)GetValue(SeriesCollectionProperty); }
set { SetValue(SeriesCollectionProperty, value); }
}
private static void OnSeriesChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var ms = sender as MsChart;
var sc = e.NewValue as BindableCollection<Series>;
if (sc != null)
sc.CollectionChanged += ms.sc_CollectionChanged;
}
private void sc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (SeriesCollection != null)
{
CheckCount = 0;
if (SeriesCollection.Count > 0)
CheckCount = SeriesCollection.Count;
}
}
private static DependencyProperty CheckCountProperty =
DependencyProperty.Register("CheckCount", typeof(int),
typeof(MsChart), new FrameworkPropertyMetadata(0, StartChart));
private int CheckCount
{
get { return (int)GetValue(CheckCountProperty); }
set { SetValue(CheckCountProperty, value); }
}
private static void StartChart(object sender, DependencyPropertyChangedEventArgs e)
{
var ms = sender as MsChart;
if (ms.CheckCount > 0)
{
ms.myChart.Visible = true;
ms.myChart.Series.Clear();
ms.myChart.Titles.Clear();
ms.myChart.Legends.Clear();
ms.myChart.ChartAreas.Clear();
MSChartHelper.MyChart(ms.myChart, ms.SeriesCollection,
ms.Title, ms.XLabel, ms.YLabel, ms.ChartBackground, ms.Y2Label);
if (ms.myChart.ChartAreas.Count > 0)
ms.myChart.ChartAreas[0].Area3DStyle.Enable3D = ms.IsArea3D;
ms.myChart.DataBind();
}
else
ms.myChart.Visible = false;
}
......
}
}
Here, we use Caliburn.Micro
as our MVVM framework. We convert the commonly used properties for the MS chart control into dependency properties. In some situations, we want to execute some logic and computation methods after setting value for a dependency property. We can perform these tasks by implementing a callback method that fires when the property changes through the property wrapper or a direct SetValue
call. For example, after creating the SeriesCollection
that contains MS chart’s Series
objects, we want the MsChart
control to create corresponding chart automatically for these Series
objects. The code snippet in the preceding code-behind file shows you how to implement such a callback method. The SeriesCollectionProperty
includes a callback method named OnSeriesChanged
. Inside this callback method, we add an event handler to the CollectionChanged
property, and it will fire when the SeriesCollection
changes. Within the CollectionChanged
handler, we set another private
dependency property called CheckCount
to the SeriesCollection.Count
. If CheckCount > 0
, we know that the SeriesCollection
does contain Series
objects, and we then implement another callback method, named StartChart
, for the CheckCount
property to create the chart by calling the MyChart
method implemented in the MSChartHelper
class. The methods included in the MSChartHelper
class simply define various pre-customized chart styles, which I will discuss in the following section.
You can bind the DataSource
dependency properties to the DataSource
property of the MS chart control. The Chart1
dependency property allows you to access the MS chart control directly if the chart type you want are not among the pre-customized chart types implemented in the MSChartHelper
class.
Helper Class
In the preceding section, we created an MsChart
control in WPF, which encapsulates the original MS chart control (the Windows Forms version). The StartChart
method in the MsChart
control calls the MyChart
method in the MSChartHelper
class. You can also create your own chart types according to the requirement of your applications by following the procedure presented here. The benefit of doing this is that you do not need to set various chart styles for every chart you create. Putting the chart-style related code in one place to form a reusable template, you can change the chart style easily. For example, if you want all of your charts to have a blue background, you simply need to change it in the template once and do not need to make any change to each of your charts.
The following code is for the MSChartHelper
class:
using System.Collections.Generic;
using System.Windows.Forms.DataVisualization.Charting;
using System.Drawing;
using Caliburn.Micro;
namespace WpfMsChart
{
public static class MSChartHelper
{
public static void MyChart(Chart chart1, BindableCollection<Series> chartSeries,
string chartTitle, string xLabel, string yLabel, ChartBackgroundColor backgroundColor,
params string[] y2Label)
{
if (chart1.ChartAreas.Count < 1)
{
ChartArea area = new ChartArea();
ChartStyle(chart1, area, backgroundColor);
}
if (chartTitle != "")
chart1.Titles.Add(chartTitle);
chart1.ChartAreas[0].AxisX.Title = xLabel;
chart1.ChartAreas[0].AxisY.Title = yLabel;
if (y2Label.Length > 0)
chart1.ChartAreas[0].AxisY2.Title = y2Label[0];
foreach (var ds in chartSeries)
chart1.Series.Add(ds);
if (chartSeries.Count > 1)
{
Legend legend = new Legend();
legend.Font = new System.Drawing.Font("Trebuchet MS", 7.0F, FontStyle.Regular);
legend.BackColor = Color.Transparent;
legend.AutoFitMinFontSize = 5;
legend.LegendStyle = LegendStyle.Column;
legend.IsDockedInsideChartArea = true;
legend.Docking = Docking.Left;
legend.InsideChartArea = chart1.ChartAreas[0].Name;
chart1.Legends.Add(legend);
}
}
public static void ChartStyle
(Chart chart1, ChartArea area, ChartBackgroundColor backgroundColor)
{
int r1 = 211;
int g1 = 223;
int b1 = 240;
int r2 = 26;
int g2 = 59;
int b2 = 105;
int r3 = 165;
int g3 = 191;
int b3 = 228;
switch (backgroundColor)
{
case ChartBackgroundColor.Blue:
chart1.BackColor = Color.FromArgb(r1, g1, b1);
chart1.BorderlineColor = Color.FromArgb(r2, g2, b2);
area.BackColor = Color.FromArgb(64, r3, g3, b3);
break;
case ChartBackgroundColor.Green:
chart1.BackColor = Color.FromArgb(g1, b1, r1);
chart1.BorderlineColor = Color.FromArgb(g2, b2, r2);
area.BackColor = Color.FromArgb(64, g3, b3, r3);
break;
case ChartBackgroundColor.Red:
chart1.BackColor = Color.FromArgb(b1, r1, g1);
chart1.BorderlineColor = Color.FromArgb(b2, r2, g2);
area.BackColor = Color.FromArgb(64, b3, r3, g3);
break;
case ChartBackgroundColor.White:
chart1.BackColor = Color.White;
chart1.BorderlineColor = Color.White;
area.BackColor = Color.White;
break;
}
if (backgroundColor != ChartBackgroundColor.White)
{
chart1.BackSecondaryColor = Color.White;
chart1.BackGradientStyle = GradientStyle.TopBottom;
chart1.BorderlineDashStyle = ChartDashStyle.Solid;
chart1.BorderlineWidth = 2;
chart1.BorderSkin.SkinStyle = BorderSkinStyle.Emboss;
area.Area3DStyle.IsClustered = true;
area.Area3DStyle.Perspective = 10;
area.Area3DStyle.IsRightAngleAxes = false;
area.Area3DStyle.WallWidth = 0;
area.Area3DStyle.Inclination = 15;
area.Area3DStyle.Rotation = 10;
}
area.AxisX.IsLabelAutoFit = false;
area.AxisX.LabelStyle.Font = new Font("Trebuchet MS", 7.25F, FontStyle.Regular);
area.AxisX.IntervalAutoMode = IntervalAutoMode.VariableCount;
area.AxisX.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisX.MajorGrid.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisX.IsStartedFromZero = false;
area.AxisX.RoundAxisValues();
area.AxisY.IsLabelAutoFit = false;
area.AxisY.LabelStyle.Font = new Font("Trebuchet MS", 7.25F,
System.Drawing.FontStyle.Regular);
area.AxisY.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisY.MajorGrid.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisY.IsStartedFromZero = false;
area.AxisY2.IsLabelAutoFit = false;
area.AxisY2.LabelStyle.Font = new Font("Trebuchet MS", 7.25F,
System.Drawing.FontStyle.Regular);
area.AxisY2.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisY2.MajorGrid.LineColor = Color.FromArgb(15, 15, 15, 15);
area.AxisY2.IsStartedFromZero = false;
area.BackSecondaryColor = System.Drawing.Color.White;
area.BackGradientStyle = GradientStyle.TopBottom;
area.BorderColor = Color.FromArgb(64, 64, 64, 64);
area.BorderDashStyle = ChartDashStyle.Solid;
area.Position.Auto = false;
area.Position.Height = 82F;
area.Position.Width = 88F;
area.Position.X = 3F;
area.Position.Y = 10F;
area.ShadowColor = Color.Transparent;
chart1.ChartAreas.Add(area);
chart1.Invalidate();
}
public static List<System.Drawing.Color> GetColors()
{
List<Color> my_colors = new List<Color>();
my_colors.Add(Color.DarkBlue);
my_colors.Add(Color.DarkRed);
my_colors.Add(Color.DarkGreen);
my_colors.Add(Color.Black);
my_colors.Add(Color.DarkCyan);
my_colors.Add(Color.DarkViolet);
my_colors.Add(Color.DarkOrange);
my_colors.Add(Color.Maroon);
my_colors.Add(Color.SaddleBrown);
my_colors.Add(Color.DarkOliveGreen);
return my_colors;
}
}
public enum ChartBackgroundColor
{
Blue = 0,
Green = 1,
Red = 2,
White = 3,
}
}
The MyChart
method takes chart1
, chartSeries
, chartTitle
, xLabel
, yLabel
, backgroundColor
, and y2Label
as input arguments; the chart1
parameter is directly assigned to the myChart
in the MsChart
control and all of other parameters are exposed to the dependency properties defined in the MsChart
control’s code-behind file. The MyChart
method also calls another method named ChartStyle
, which defines various chart-style related properties including background color, label fonts, chart area appearance, gridlines, etc. Here, we have implemented four background colors, Blue
, Green
, Red
, and White
, using a ChartBackgroundColorEnum
. You can easily add more chart types and background colors as you like. We also create a list of ten predefined colors in the GetColors
method, which we can use to specify the colors for the chart series.
Creating Charts Using WPF MsChart Control
In this section, I will use an example to show you how to create several different charts using the WPF MsChart
control implemented in the preceding sections. The following is the XAML file for the view named MainView
of this example:
<Window x:Class="WpfMsChart.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfMsChart"
xmlns:cal="http://www.caliburnproject.org"
mc:Ignorable="d"
Title="MainView" Height="300" Width="500">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel>
<Button x:Name="BarChart" Content="Bar Chart" Width="120" Margin="0 10 0 0"/>
<Button x:Name="LineChart" Content="Line Chart" Width="120" Margin="0 10 0 0"/>
<Button x:Name="PieChart" Content="Pie Chart" Width="120" Margin="0 10 0 0"/>
<Button x:Name="PolarChart" Content="Polar Chart" Width="120" Margin="0 10 0 0"/>
</StackPanel>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<local:MsChart SeriesCollection="{Binding BarSeriesCollection}"
ChartBackground="Blue" Title="Bar Chart"/>
<local:MsChart SeriesCollection="{Binding LineSeriesCollection}"
ChartBackground="Red" Title="Line Chart" Grid.Column="1"/>
<local:MsChart SeriesCollection="{Binding PieSeriesCollection}"
ChartBackground="Green" Title="Pie Chart" Grid.Row="1" IsArea3D="True"/>
<local:MsChart SeriesCollection="{Binding PolarSeriesCollection}"
ChartBackground="White" Title="Polar Chart" XLabel="" YLabel=""
Grid.Row="1" Grid.Column="1"/>
</Grid>
</Grid>
</Window>
Using the XAML namespace, local
, and the user control’s class name, MsChart
, we add the control exactly as we would add any other type of object to the XAML file, even though the MsChart
control contains the Windows Forms MS chart control that was hosted in a WindowsFormsHost
. You can see that in this example, we want to create four simple charts, including bar, line, pie, and polar charts. In this case, we need to specify one key property, SeriesCollection
, which holds the chart series and should be defined in the view model.
Here is the code for the corresponding view model:
using System;
using Caliburn.Micro;
using System.Windows.Forms.DataVisualization.Charting;
namespace WpfMsChart
{
public class MainViewModel : PropertyChangedBase
{
public BindableCollection<Series> BarSeriesCollection { get; set; }
public BindableCollection<Series> LineSeriesCollection { get; set; }
public BindableCollection<Series> PieSeriesCollection { get; set; }
public BindableCollection<Series> PolarSeriesCollection { get; set; }
public MainViewModel()
{
BarSeriesCollection = new BindableCollection<Series>();
LineSeriesCollection = new BindableCollection<Series>();
PieSeriesCollection = new BindableCollection<Series>();
PolarSeriesCollection = new BindableCollection<Series>();
}
public void BarChart()
{
double[] data1 = new double[] { 32, 56, 35, 12, 35, 6, 23 };
double[] data2 = new double[] { 67, 24, 12, 8, 46, 14, 76 };
BarSeriesCollection.Clear();
Series ds = new Series();
ds.ChartType = SeriesChartType.Column;
ds["DrawingStyle"] = "Cylinder";
ds.Points.DataBindY(data1);
BarSeriesCollection.Add(ds);
ds = new Series();
ds.ChartType = SeriesChartType.Column;
ds["DrawingStyle"] = "Cylinder";
ds.Points.DataBindY(data2);
BarSeriesCollection.Add(ds);
}
public void LineChart()
{
LineSeriesCollection.Clear();
Series ds = new Series();
ds.ChartType = SeriesChartType.Line;
ds.BorderDashStyle = ChartDashStyle.Solid;
ds.MarkerStyle = MarkerStyle.Diamond;
ds.MarkerSize = 8;
ds.BorderWidth = 2;
ds.Name = "Sine";
for (int i = 0; i < 70; i++)
{
double x = i / 5.0;
double y = 1.1 * Math.Sin(x);
ds.Points.AddXY(x, y);
}
LineSeriesCollection.Add(ds);
ds = new Series();
ds.ChartType = SeriesChartType.Line;
ds.BorderDashStyle = ChartDashStyle.Dash;
ds.MarkerStyle = MarkerStyle.Circle;
ds.MarkerSize = 8;
ds.BorderWidth = 2;
ds.Name = "Cosine";
for (int i = 0; i < 70; i++)
{
double x = i / 5.0;
double y = 1.1 * Math.Cos(x);
ds.Points.AddXY(x, y);
}
LineSeriesCollection.Add(ds);
}
public void PieChart()
{
PieSeriesCollection.Clear();
Random random = new Random();
Series ds = new Series();
for (int i = 0; i < 5; i++)
ds.Points.AddY(random.Next(10, 50));
ds.ChartType = SeriesChartType.Pie;
ds["PointWidth"] = "0.5";
ds.IsValueShownAsLabel = true;
ds["BarLabelStyle"] = "Center";
ds["DrawingStyle"] = "Cylinder";
PieSeriesCollection.Add(ds);
}
public void PolarChart()
{
PolarSeriesCollection.Clear();
Series ds = new Series();
ds.ChartType = SeriesChartType.Polar;
ds.BorderWidth = 2;
for (int i = 0; i < 360; i++)
{
double x = 1.0 * i;
double y = 0.001 + Math.Abs(Math.Sin(2.0 * x * Math.PI / 180.0) *
Math.Cos(2.0 * x * Math.PI / 180.0));
ds.Points.AddXY(x, y);
}
PolarSeriesCollection.Add(ds);
}
}
}
Here, we define four Series
collections and four methods, which are used to create the bar, line, pie, and polar charts. If you have ever used the MS chart control in the Windows Forms applications before, you should be familiar with the code inside each method. We create the Series
objects inside each method and add the series objects to the corresponding series collections, which are data bound to the MsChart
control defined in the view. This way, we can add MS charts to our WPF applications with the MVVM compliance.
Running this example generates results shown in the following figure:
Conclusion
Here, I have presented the detailed procedure on how to convert the MS chart control into a WPF and MVVM compatible chart control and how to use it to create various charts in a WPF application.