Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Compatible MS Chart Control

0.00/5 (No votes)
9 Jan 2017 1  
This article demonstrates how to convert the WinForm version of the Microsoft chart control into a WPF and MVVM compatible chart control and how to use it to create various charts in a WPF application.

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
{
    /// <summary>
    /// Interaction logic for MsChart.xaml
    /// </summary>
    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.LabelStyle.IsEndLabelVisible = false;
            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, GreenRed, 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here