Click here to Skip to main content
13,150,178 members (56,859 online)
Click here to Skip to main content
Add your own
alternative version

Stats

7.1K views
329 downloads
17 bookmarked
Posted 5 Jan 2017

WPF Compatible MS Chart Control

, 9 Jan 2017
Rate this:
Please Sign up or sign in to vote.
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 specilized 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 encapsulate 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 precedure 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.  Recently, I have used this WPF compatible chart control in my recent book "Practical C# and WPF for Financial Markets" to visualize the stock charts and technical indicators. If you are interested in creating WPF MS chart control with more features, please visit my website at www.DrXuDotNet.com for more information.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Jack Xu, USA
www.drxudotnet.com
United States United States
Jack Xu has a PhD in theoretical physics. He has over 25-year programming experience in Basic, FORTRAN, C, C++, Matlab, C#, WPF, and R, specializing in numerical computation methods, algorithms, physical modeling, computer aided design tools, graphical user interfaces, 3D graphics, and database systems. In recent 7 years, he works as a quant analyst and developer on Wall Street and is responsible for quantitative analysis, back-testing, trading strategy development, and real-time trading system design and implementation using .NET technology.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionUserControl without caliburn framework? Pin
marinschek4-Apr-17 1:07
membermarinschek4-Apr-17 1:07 
BugDownloading project failed!? Pin
Tokinabo9-Jan-17 9:16
professionalTokinabo9-Jan-17 9:16 
GeneralRe: Downloading project failed!? Pin
Jack Xu, USA9-Jan-17 15:53
memberJack Xu, USA9-Jan-17 15:53 
QuestionConcerning WPF toolkit Pin
Mika Wendelius6-Jan-17 7:31
mvpMika Wendelius6-Jan-17 7:31 
AnswerRe: Concerning WPF toolkit Pin
Jack Xu, USA9-Jan-17 6:17
memberJack Xu, USA9-Jan-17 6:17 
QuestionExcellent Pin
John Simmons / outlaw programmer6-Jan-17 6:24
memberJohn Simmons / outlaw programmer6-Jan-17 6:24 
QuestionMissing image Pin
Mika Wendelius6-Jan-17 0:15
mvpMika Wendelius6-Jan-17 0:15 
AnswerRe: Missing image Pin
Jack Xu, USA6-Jan-17 2:59
memberJack Xu, USA6-Jan-17 2:59 
PraiseRe: Missing image Pin
Mika Wendelius6-Jan-17 7:24
mvpMika Wendelius6-Jan-17 7:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.170924.2 | Last Updated 9 Jan 2017
Article Copyright 2017 by Jack Xu, USA
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid