Introduction
WPF provides a unified graphics platform that allows you to easily create a variety of user interfaces and graphics objects in your .NET applications. Here, I will present a step-by-step tutorial on how to create a line chart control in WPF. We require this chart control to behave like WPF built-in elements: It can be created in XAML; data bound to properties defined in the view model; and MVVM-compatible. I will begin by describing a 2D chart coordinate systems used in WPF; then show you how to create simple line charts in this coordinate system; and finally show you how to create a line chart control and how to use the chart control in WPF applications with MVVM pattern.
Coordinate System for 2D Charts<o:p>
The custom coordinate system used in 2D chart applications must satisfy the following conditions: It must be independent of the unit of the real-world graphics objects, and its Y-axis must point from bottom to top, as it does in most chart applications. Figure 1 illustrates this custom coordinate system.
Figure 1. Custome coordinate system for 2D chart applications.
You can see that we define the real-world X-Y coordinate system within a rendering area. You can create such a coordinate system using a custom panel control by overriding its MeasureOverride and ArrangeOverride methods. Each method returns the size data needed to position and render child elements. This is a standard method for creating custom coordinate systems. Instead of creating a custom panel control, here I will construct this coordinate system using a different approach, based on direct coding. In next section, I will create a simple line chart that illustrates how to construct the custom 2D chart coordinate system.
Simple Line Charts
The X-Y line chart uses two values to represent each data point. This type of chart is very useful for describing relationships between data and is often involved in the statistical analysis of data, with wide applications in the scientific, mathematics, engineering, and finance communities as well as in daily life.
Open Visual Studio 2013, start a new WPF project, and name it WpfChart. I’ll use Caliburn.Micro as our MVVM framework and will not go to details on how to use Caliburn.Micro. You can go to their website for more information (https://github.com/Caliburn-Micro). Add a UserControl to the project and name it SimpleChartView. The following is code snippet in XAML for creating the line chart:
<Grid ClipToBounds="True" cal:Message.Attach="[Event SizeChanged]=
[Action AddChart($this.ActualWidth, $this.ActualHeight)];
[Event Loaded]=[Action AddChart($this.ActualWidth, $this.ActualHeight)]">
<Polyline Points="{Binding SolidLinePoints}" Stroke="Black" StrokeThickness="2"/>
<Polyline Points="{Binding DashLinePoints}" Stroke="Black"
StrokeThickness="2" StrokeDashArray="4,3"/>
</Grid>
Here we add two Polylines to the Grid, which are bound to two PointCollection objects, SolidLinePoints and DashLinePoints, respectively. You may also notice that I use Caliburn.Micro’s action mechanism to bind UI events in view to methods defined in the view model. Here I use Message.Attach property to bind the Grid’s Loaded and SizeChanged events to the AddChart method in the view model, and pass the Grid’s ActualWidth and ActualHeight properties to the AddChart method. This action will fire whenever the Grid is loaded or resized, resulting in recreating the point collections and redrawing the chart on your screen.
Add a new class to the project and name it SimpleChartViewModel. Here is the code for this class:
using System;
using Caliburn.Micro;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
namespace WpfChart
{
[Export(typeof(IScreen)), PartCreationPolicy(CreationPolicy.NonShared)]
public class SimpleChartViewModel : Screen
{
[ImportingConstructor]
public SimpleChartViewModel()
{
DisplayName = "01. Simple Line";
}
private double chartWidth = 300;
private double chartHeight = 300;
private double xmin = 0;
private double xmax = 6.5;
private double ymin = -1.1;
private double ymax = 1.1;
private PointCollection solidLinePoints;
public PointCollection SolidLinePoints
{
get { return solidLinePoints; }
set
{
solidLinePoints = value;
NotifyOfPropertyChange(() => SolidLinePoints);
}
}
private PointCollection dashLinePoints;
public PointCollection DashLinePoints
{
get { return dashLinePoints; }
set
{
dashLinePoints = value;
NotifyOfPropertyChange(() => DashLinePoints);
}
}
public void AddChart(double width, double height)
{
chartWidth = width;
chartHeight = height;
SolidLinePoints = new PointCollection();
DashLinePoints = new PointCollection();
double x = 0;
double y = 0;
double z = 0;
for (int i = 0; i < 70; i++)
{
x = i / 5.0;
y = Math.Sin(x);
z = Math.Cos(x);
DashLinePoints.Add(NormalizePoint(new Point(x, z)));
SolidLinePoints.Add(NormalizePoint(new Point(x, y)));
}
}
public Point NormalizePoint(Point pt)
{
var res = new Point();
res.X = (pt.X - xmin) * chartWidth / (xmax - xmin);
res.Y = chartHeight - (pt.Y - ymin) * chartHeight / (ymax - ymin);
return res;
}
}
}
Note that the axis limits xmin, xmax, ymin, and ymax are defined in the real-world coordinate system. The Sine and Cosine functions are represented using two point collections, SolidLinePoints and DashLinePoints, which are bound to Polyline objects.
Pay special attention to the AddChart method. We first set the chartWidth and chartHeight to the Grid’s ActualWidth and ActualHeight properties, ensuring that the chart size will change when the Gird is resized. We also recreate the point collections, so the chart will redraw when you resize the screen.
A key step in creating this line chart is to transform the original data points in the world coordinate system into points in the units of device-independent pixels using the NormalizePoint method. The NormalizePoint method converts points of any unit in the world coordinate system into points with a unit of device-independent pixel in the device coordinate system.
Figure 2 shows the results of running this example.
Figure 2. A simple 2D chart for Sime and Cosine functions.
Line Charts with Chart Style
The preceding example demonstrated how easy it is to create a simple 2D line chart in WPF using the standard MVVM pattern with a perfect separation between the view and the view model.
In order for the chart program to be more object-oriented and to extend easily to add new features, we need to define two new classes: ChartStyle and LineSeries. The LineSeries class holds the chart data and line styles, including the line color, thickness, dash style, etc. The ChartStyle class defines all chart layout–related information including gridlines, a title, tick marks, and labels for axes. To this end, we need to dynamically create many controls on the view, which is hard to create these controls in XAML. Therefore, we want the view model to be able to access the view and add controls dynamically to the view. This may violate the MVVM rules, and I will show you later that we can still create MVVM-compatible chart applications when we convert chart applications into a chart user control.
LineSeries Class
Add a new folder to the project and name it ChartModel. Add a new class to the ChartModel folder and name it LineSeries. Here is the code for this class:
using System;
using System.Windows.Media;
using System.Windows.Shapes;
using Caliburn.Micro;
using System.Windows;
namespace WpfChart.ChartModel
{
public class LineSeries : PropertyChangedBase
{
public LineSeries()
{
LinePoints = new BindableCollection<Point>();
}
public BindableCollection<Point> LinePoints { get; set; }
private Brush lineColor = Brushes.Black;
public Brush LineColor
{
get { return lineColor; }
set { lineColor = value; }
}
private double lineThickness = 1;
public double LineThickness
{
get { return lineThickness; }
set { lineThickness = value; }
}
public LinePatternEnum LinePattern { get; set; }
private string seriesName = "Default";
public string SeriesName
{
get { return seriesName; }
set { seriesName = value; }
}
private DoubleCollection lineDashPattern;
public DoubleCollection LineDashPattern
{
get { return lineDashPattern; }
set
{
lineDashPattern = value;
NotifyOfPropertyChange(() => LineDashPattern);
}
}
public void SetLinePattern()
{
switch (LinePattern)
{
case LinePatternEnum.Dash:
LineDashPattern = new DoubleCollection() { 4, 3 };
break;
case LinePatternEnum.Dot:
LineDashPattern = new DoubleCollection() { 1, 2 };
break;
case LinePatternEnum.DashDot:
LineDashPattern = new DoubleCollection() { 4, 2, 1, 2 };
break;
}
}
}
public enum LinePatternEnum
{
Solid = 1,
Dash = 2,
Dot = 3,
DashDot = 4,
}
}
This class creates a point collection object called LinePoints for a given LineSeries. It then defines the line style for the line object, including the line color, thickness, line pattern, and series name. The SeriesName property will be used when creating the legend for the chart. The line pattern is defined by a public enumeration called LinePatternEnum, in which four line patterns are defined, including Solid, Dash, Dot, and DashDot.
We create the line pattern via the SetLinePattern method. There is no need to create the solid line pattern because it is the default setting of the Polyline object. We also create the dashed or dotted line patterns using the StrokeDashArray property of the Polyline.
ChartStyle Class
Add another new class, ChartStyle, to the ChartModel folder. The following is the code listing for this class:
using Caliburn.Micro;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace WpfChart.ChartModel
{
public class ChartStyle
{
private double xmin = 0;
private double xmax = 6.5;
private double ymin = -1.1;
private double ymax = 1.1;
private string title = "Title";
private string xLabel = "X Axis";
private string yLabel = "Y Axis";
private bool isXGrid = true;
private bool isYGrid = true;
private Brush gridlineColor = Brushes.LightGray;
private double xTick = 1;
private double yTick = 0.5;
private LinePatternEnum gridlinePattern;
private double leftOffset = 20;
private double bottomOffset = 15;
private double rightOffset = 10;
private Line gridline = new Line();
public Canvas TextCanvas { get; set; }
public Canvas ChartCanvas { get; set; }
public double Xmin
{
get { return xmin; }
set { xmin = value; }
}
... ...
... ...
public void AddChartStyle(TextBlock tbTitle, TextBlock tbXLabel, TextBlock tbYLabel)
{
Point pt = new Point();
Line tick = new Line();
double offset = 0;
double dx, dy;
TextBlock tb = new TextBlock();
tb.Text = Xmax.ToString();
tb.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
Size size = tb.DesiredSize;
rightOffset = 5;
for (dy = Ymin; dy <= Ymax; dy += YTick)
{
pt = NormalizePoint(new Point(Xmin, dy));
tb = new TextBlock();
tb.Text = dy.ToString();
tb.TextAlignment = TextAlignment.Right;
tb.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
size = tb.DesiredSize;
if (offset < size.Width)
offset = size.Width;
}
leftOffset = offset + 5;
Canvas.SetLeft(ChartCanvas, leftOffset);
Canvas.SetBottom(ChartCanvas, bottomOffset);
ChartCanvas.Width = Math.Abs(TextCanvas.Width - leftOffset - rightOffset);
ChartCanvas.Height = Math.Abs(TextCanvas.Height - bottomOffset - size.Height / 2);
... ...
... ...
}
public Point NormalizePoint(Point pt)
{
if (Double.IsNaN(ChartCanvas.Width) || ChartCanvas.Width <= 0)
ChartCanvas.Width = 270;
if (Double.IsNaN(ChartCanvas.Height) || ChartCanvas.Height <= 0)
ChartCanvas.Height = 250;
Point result = new Point();
result.X = (pt.X - Xmin) * ChartCanvas.Width / (Xmax - Xmin);
result.Y = ChartCanvas.Height - (pt.Y - Ymin) * ChartCanvas.Height / (Ymax - Ymin);
return result;
}
public void SetLines(BindableCollection<LineSeries> dc)
{
if (dc.Count <= 0)
return;
int i = 0;
foreach (var ds in dc)
{
PointCollection pts = new PointCollection();
if (ds.SeriesName == "Default")
ds.SeriesName = "LineSeries" + i.ToString();
ds.SetLinePattern();
for (int j = 0; j < ds.LinePoints.Count; j++)
{
var pt = NormalizePoint(ds.LinePoints[j]);
pts.Add(pt);
}
Polyline line = new Polyline();
line.Points = pts;
line.Stroke = ds.LineColor;
line.StrokeThickness = ds.LineThickness;
line.StrokeDashArray = ds.LineDashPattern;
ChartCanvas.Children.Add(line);
i++;
}
}
}
}
Here, I only list part of the code in this class. You can view the complete code listing in the attached zip file. Note that we define the Canvas control, ChartCanvas, as a public property. Usually, the MVVM framework does not allow the Canvas control to appear in the model or view model. As discussed previously, the purpose for doing so is to be able to add controls dynamically.
Here, we add more member fields and corresponding properties, which we use to manipulate the chart’s layout and appearance. You can easily understand the meaning of each field and property from its name. Notice that I add another Canvas property, TextCanvas, which we use to hold the tick mark labels, while the ChartCanvas holds the chart itself.
In addition, I add the following member fields to define the gridlines for the chart:
private bool isXGrid = true;
private bool isYGrid = true;
private Brush gridlineColor = Brushes.LightGray;
private LinePatternEnum gridlinePattern;
These fields and their corresponding properties provide a great deal of flexibility in customizing the appearance of the gridlines. The GridlinePattern property allows you to choose various line dash styles, including solid, dash, dot, and dash-dot. You can change the gridlines’ color using the GridlineColor property. In addition, I define two bool properties, IsXGrid and IsYGrid, which allow you to turn horizontal or vertical gridlines on or off.
I then define member fields and corresponding properties for the X and Y labels, title, and tick marks so that you can change them to your liking. If you like, you can easily add more member fields to control the appearance of the charts; for example, you can change the font and text color of the labels and title.
The AddChartStyle method seems quite complicated in this class; however, it is actually reasonably easy to follow. First, I make a lot of effort to define the size of the ChartCanvas by considering the suitable offset relative to the TextCanvas.
ChartCanvas.Width = Math.Abs(TextCanvas.Width – leftOffset - rightOffset);
ChartCanvas.Height = Math.Abs(TextCanvas.Height – bottomOffset - size.Height / 2);
Next, I draw gridlines with a specified color and line pattern. Please note that all of the end points of the gridlines have been transformed from the world coordinate system to device-independent pixels using the NormalizePoint method.
I then draw the tick marks for the X and Y axes of the chart. For each tick mark, I find the points in the device coordinate system at which the tick mark joins the axes and draw a black line, 5 pixels long, from this point toward the inside of the ChartCanvas.
The title and labels for the X- and Y-axes are attached to the corresponding TextBlock names in code. You can also create data bindings that bind the Title, XLabel, and YLabel properties to the corresponding TextBlock directly in the XAML file.
You can clearly see from the preceding code that this class involves dynamically creating and positioning controls, which is hard to achieve using XAML in the view. Here we choose a simple approach that we do all the dynamical creation and placement for the controls in model or view model classes. In some situations, like the case in this example, you do not need to force you to follow the MVVM rules, in particular when the rules make the simple problem complicated.
Creating Line Charts With Chart Style
Now we can use the above two classes, LineSeries and ChartStyle, to create a line chart with gridlines, axis labels, title, and tick marks. Add a new UserControl to the project and name it ChartView. Here is the relevant code snippet in XAML for this view:
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Margin="2" x:Name="tbTitle" Grid.Column="1" Grid.Row="0"
RenderTransformOrigin="0.5,0.5" FontSize="14" FontWeight="Bold"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
TextAlignment="Center" Text="Title"/>
<TextBlock Margin="2" x:Name="tbXLabel" Grid.Column="1" Grid.Row="2"
RenderTransformOrigin="0.5,0.5" TextAlignment="Center" Text="X Axis"/>
<TextBlock Margin="2" Name="tbYLabel" Grid.Column="0" Grid.Row="1"
RenderTransformOrigin="0.5,0.5" TextAlignment="Center" Text="Y Axis">
<TextBlock.LayoutTransform>
<RotateTransform Angle="-90"/>
</TextBlock.LayoutTransform>
</TextBlock>
<Grid Margin="0,0,0,0" x:Name ="chartGrid" Grid.Column="1" Grid.Row="1"
ClipToBounds="False" Background="Transparent"
cal:Message.Attach="[Event SizeChanged]=[Action AddChart];
[Event Loaded]=[Action AddChart]">
<Canvas Margin="2" Name="textCanvas" Grid.Column="1" Grid.Row="1" ClipToBounds="True"
Width="{Binding ElementName=chartGrid,Path=ActualWidth}"
Height="{Binding ElementName=chartGrid,Path=ActualHeight}">
<Canvas Name="chartCanvas" ClipToBounds="True"/>
</Canvas>
</Grid>
</Grid>
Here, we put the title and labels for the X- and Y-axes into different cells of a Grid control, and define two canvas controls: textCanvas and chartCanvas. The textCanvas becomes a resizable Canvas control because its Width and Height properties are bound to chartGrid’s ActualWidth and ActualHeight properties. We use the textCanvas control, as a parent of the chartCanvas, to hold the tick mark labels; the chartCanvas control will hold the chart itself.
Add a new class to the project and name it ChartViewModel. Here is the code for this view:
using System;
using Caliburn.Micro;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Windows.Documents;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using WpfChart.ChartModel;
namespace WpfChart
{
[Export(typeof(IScreen)), PartCreationPolicy(CreationPolicy.NonShared)]
public class ChartViewModel :Screen
{
[ImportingConstructor]
public ChartViewModel()
{
DisplayName = "02. Chart";
}
private ChartView view;
private ChartStyle cs;
private void SetChartStyle()
{
view = this.GetView() as ChartView;
view.chartCanvas.Children.Clear();
view.textCanvas.Children.RemoveRange(1, view.textCanvas.Children.Count - 1);
cs = new ChartStyle();
cs.ChartCanvas = view.chartCanvas;
cs.TextCanvas = view.textCanvas;
cs.Title = "Sine and Cosine Chart";
cs.Xmin = 0;
cs.Xmax = 7;
cs.Ymin = -1.5;
cs.Ymax = 1.5;
cs.YTick = 0.5;
cs.GridlinePattern = LinePatternEnum.Dot;
cs.GridlineColor = Brushes.Green;
cs.AddChartStyle(view.tbTitle, view.tbXLabel, view.tbYLabel);
}
public void AddChart()
{
SetChartStyle();
BindableCollection<LineSeries> dc = new BindableCollection<LineSeries>();
var ds = new LineSeries();
ds.LineColor = Brushes.Blue;
ds.LineThickness = 2;
ds.LinePattern = LinePatternEnum.Solid;
for (int i = 0; i < 50; i++)
{
double x = i / 5.0;
double y = Math.Sin(x);
ds.LinePoints.Add(new Point(x, y));
}
dc.Add(ds);
ds = new LineSeries();
ds.LineColor = Brushes.Red;
ds.LineThickness = 2;
ds.LinePattern = LinePatternEnum.Dash;
ds.SetLinePattern();
for (int i = 0; i < 50; i++)
{
double x = i / 5.0;
double y = Math.Cos(x);
ds.LinePoints.Add(new Point(x, y));
}
dc.Add(ds);
cs.SetLines(dc);
}
}
}
Pay special attention to how we access the view from the view model. Caliburn.Micro provides a method called GetView that allows us to access the view easily. As discussed previously, the ChartStyle class needs to access the textCanvas and chartCanvas controls that are created in the view, so within the SetChartStyle method we access the view using the following code snippet:
view = this.GetView() as ChartView;
view.chartCanvas.Children.Clear();
view.textCanvas.Children.RemoveRange(1, view.textCanvas.Children.Count - 1);
cs = new ChartStyle();
cs.ChartCanvas = view.chartCanvas;
cs.TextCanvas = view.textCanvas;
First we get view object via the GetView method. In order to redraw the chart when the application window is resized, we need to recreate all of the children elements of the textCanvas but the chartCanvas, and remove all of the children controls of the chartCanvas, which is achieved by means of the bolded code statements in the foregoing code snippet. We then create the ChartStyle object and set its ChartCanvas and TextCanvas properties to the view’s corresponding controls.
Within the AddChart method, we first call the SetChartStyle method that set the gridlines, title, tick marks and axis labels. The rest of the code is similar to that used in the previous example. Figure 3 illustrates the results of running this application. You can see that the chart has a title, labels, gridlines, and tick marks. Congratulations! You have successfully created a line chart in WPF.
Figure 3. A line chart with gridlines and tick marks.
Line Chart Control
In the preceding sections, we implemented the source code for all of the classes in our chart programs directly. For simple applications, this approach works well. However, if you want to reuse the same code in multiple .NET applications, this method becomes ineffective. The .NET framework and WPF provide a powerful means, the user control, to solve this problem.
Custom user controls in WPF are just like the simple buttons or text boxes already provided with .NET and WPF. Typically, the controls you design are to be used in multiple windows or modularize your code. These custom controls can reduce the amount of code you have to type as well as make it easier for you to change the implementation of your program. There is no reason to duplicate code in your applications because this leaves a lot of room for bugs. Therefore, it is a good programming practice to create functionality specific to the user control in the control's source code, which can reduce code duplication and modularize your code.
In this section, I’ll show you how to put the line charts into a custom user control and how to use such a control in your WPF applications with MVVM style. We will try to make the chart control into a first-class WPF citizen and make it available in XAML. This means that we will need to define dependency properties and routed events for the chart control in order to get support for essential WPF services, such as data binding, styles, and animation.
It is easy to create a line chart control based on the line charts we developed previously. The development model for the chart user control is very similar to the model used for application development in WPF.
Right click the WpfChart solution and choose Add | New Project…to bring up the Add New Project dialog. You need to select a new WPF User Control Library from templates and name it ChartControl. When you do this, Visual Studio 2013 creates an XAML markup file and a corresponding custom class to hold your initialization and event-handling code. This will generate a default user control named UserControl1. Rename it LineChart by right clicking on the UserControl1.xaml in the solution explorer and selecting Rename. You need also to change the name UserControl1 to LineChart in both the XAML file and the code-behind file for the control. Add a new folder to the control and name it ChartModel. Add two existing classes from the ChartModel folder of the WpfChart project to the ChartModel folder of the current project, and change the namespace to ChartControl for all of these classes in the ChartModel folder of the current project. To avoid confusion, you also need to change the names of these two classes to LineSeriesControl and ChartStyleControl.
<UserControl x:Class="ChartControl.LineChart"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Margin="2" x:Name="tbTitle" Grid.Column="1" Grid.Row="0"
RenderTransformOrigin="0.5,0.5" FontSize="14" FontWeight="Bold"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
TextAlignment="Center" Text="Title"/>
<TextBlock Margin="2" x:Name="tbXLabel" Grid.Column="1" Grid.Row="2"
RenderTransformOrigin="0.5,0.5" TextAlignment="Center" Text="X Axis"/>
<TextBlock Margin="2" Name="tbYLabel" Grid.Column="0" Grid.Row="1"
RenderTransformOrigin="0.5,0.5" TextAlignment="Center" Text="Y Axis">
<TextBlock.LayoutTransform>
<RotateTransform Angle="-90"/>
</TextBlock.LayoutTransform>
</TextBlock>
<Grid Margin="0,0,0,0" x:Name ="chartGrid" Grid.Column="1" Grid.Row="1"
ClipToBounds="False" Background="Transparent" SizeChanged="chartGrid_SizeChanged">
<Canvas Margin="2" Name="textCanvas" Grid.Column="1" Grid.Row="1" ClipToBounds="True"
Width="{Binding ElementName=chartGrid,Path=ActualWidth}"
Height="{Binding ElementName=chartGrid,Path=ActualHeight}">
<Canvas Name="chartCanvas" ClipToBounds="True"/>
</Canvas>
</Grid>
</Grid>
</UserControl>
You may note that we are going to use the code-behind code to implement the chart control because we do not use the Caliburn.Micro’s Message.Attach method for the event handler, such as the SizeChanged event. In fact, we have two options in creating a user control library. If the chart control we design is consumed only in the current application, we can then create the user control using MVVM approach. In this case, we need to create separate view models for the control and an instance of it in our parent application that will consume the control. So, the parent view will have this control in it and will bind the control’s view model to the control via ParentVM.UserControlVM, and our user control will take care of other bindings.
On the other hand, if our control will be used by other applications or by other developers, we then need to create our user control by following the control template implementation based on dependency properties. Here, I should point out an important point that weather we decide to use MVVM or code-behind dependency properties to develop our chart control, it will not break the MVVM rules for the consumers of our user control.
Here, I will show you how to create the chart control based on dependency properties implemented in the code-behind code. The dependency properties provide a simple way for data binding when the source object is a WPF element and the source property is a dependency property. This is because dependency properties have a built-in support for property-changed notification. As a result, changing the value of the dependency property in the source object updates the bound property in the target object immediately. That is exactly what we want – and it happens without requiring us to build any additional infrastructure, such as an INotifyPropertyChanged interface.
Defining Dependency Properties
Next, we will implement the public interface that the line chart control exposes to the outside world. In other words, it is time to create the properties, methods, and events that the control consumer (the application that uses the control) will rely on to interact with our chart control.
We may want to expose to outside world most of the properties in the ChartStyleControl class, such as axis limits, title, and labels; at the same time, we will try to make as few changes as possible to the original line chart example project.
The first step in creating a dependency property is to define a static field for it, with the word Property added to the end of the property name. Add the following code to the code-behind file:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using Caliburn.Micro;
using System.Collections.Specialized;
namespace ChartControl
{
public partial class LineChart : UserControl
{
private ChartStyleControl cs;
public LineChart()
{
InitializeComponent();
this.cs = new ChartStyleControl();
this.cs.TextCanvas = textCanvas;
this.cs.ChartCanvas = chartCanvas;
}
private void chartGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
ResizeLineChart();
}
private void SetLineChart()
{
cs.Xmin = this.Xmin;
cs.Xmax = this.Xmax;
cs.Ymin = this.Ymin;
cs.Ymax = this.Ymax;
cs.XTick = this.XTick;
cs.YTick = this.YTick;
cs.XLabel = this.XLabel;
cs.YLabel = this.YLabel;
cs.Title = this.Title;
cs.IsXGrid = this.IsXGrid;
cs.IsYGrid = this.IsYGrid;
cs.GridlineColor = this.GridlineColor;
cs.GridlinePattern = this.GridlinePattern;
ResizeLineChart();
}
private void ResizeLineChart()
{
chartCanvas.Children.Clear();
textCanvas.Children.RemoveRange(1, textCanvas.Children.Count - 1);
cs.AddChartStyle(tbTitle, tbXLabel, tbYLabel);
if (DataCollection != null)
{
if (DataCollection.Count > 0)
{
cs.SetLines(DataCollection);
}
}
}
public static DependencyProperty XminProperty =
DependencyProperty.Register("Xmin", typeof(double), typeof(LineChart),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public double Xmin
{
get { return (double)GetValue(XminProperty); }
set { SetValue(XminProperty, value); }
}
......
......
public static readonly DependencyProperty DataCollectionProperty =
DependencyProperty.Register("DataCollection",
typeof(BindableCollection<LineSeriesControl>), typeof(LineChart),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnDataChanged));
public BindableCollection<LineSeriesControl> DataCollection
{
get { return (BindableCollection<LineSeriesControl>)GetValue(DataCollectionProperty); }
set { SetValue(DataCollectionProperty, value); }
}
private static void OnDataChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var lc = sender as LineChart;
var dc = e.NewValue as BindableCollection<LineSeriesControl>;
if (dc != null)
dc.CollectionChanged += lc.dc_CollectionChanged;
}
private void dc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (DataCollection != null)
{
CheckCount = 0;
if (DataCollection.Count > 0)
CheckCount = DataCollection.Count;
}
}
public static DependencyProperty CheckCountProperty =
DependencyProperty.Register("CheckCount", typeof(int), typeof(LineChart),
new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(OnStartChart)));
public int CheckCount
{
get { return (int)GetValue(CheckCountProperty); }
set { SetValue(CheckCountProperty, value); }
}
private static void OnStartChart(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
(sender as LineChart).SetLineChart();
}
}
}
Here I only want to show you how to create dependency properties and have omitted most repeated code for defining dependency properties. You can check the complete code list from the attached project program. Note how we define the dependency property for Xmin. WPF comes with a completely new technique of defining the properties for a control. The core of the new property system is the dependency property and the wrapper class called a DepedencyObject. Here, we use the wrapper class to register the Xmin dependency property into a property system to ensure that the object contains the property in it, and we can easily get or set the value of the property. Usually, the property wrapper should not contain any logic, because properties may be set and retrieved directly using the SetValue and GetValue methods of the base DependencyObject class.
In some situations, however, we may 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 DataCollection that contains LineSeriesControl objects, we want the chart control to create corresponding line chart automatically for these LineSeriesControl objects. The bolded code in the preceding code-behind file shows how to implement such a callback method. The DataCollectionProperty includes a callback method named OnDataChanged. Inside this callback method, we add an event handler to the CollectionChanged property, and it will fire when the DataCollection changes. Within the CollectionChanged handler, we set another private dependency property called CheckCount to the DataCollection.Count. If CheckCount > 0, we know that the DataCollection does contain LineSeries objects, and we then implement another callback method named OnStartChart for the CheckCount property to create the line chart by calling the SetLineChart method.
Note that inside the SetLineChart method, we set public properties in the ChartStyleControl class to corresponding dependency properties of the chart control. This way, whenever the dependency properties in the LineChart class change, the properties of the ChartStyle and Legend classes will change correspondingly.
Here, we also add a chartGrid_SizeChanged event handler to the line chart control. This handler ensures that the chart gets updated whenever the chart control is resized. Now we can build the control library by right clicking the ChartControl project and selecting Build.
Using Chart Control
Now that we have created the line chart control, we can easily use it in our WpfChart project. To use the control in a WPF application, we need to map the .NET namespace and assembly to an XML namespace, as shown here:
xmlns:local="clr-namespace:ChartControl;assembly=ChartControl"
If the chart control is located in the same assembly as our application, we only need to map the namespace:
xmlns:local="clr-namespace:ChartControl
Using the XML namespace and the user control class name, you can add the user control exactly as you would add any other type of object to the XAML file.
Creating a Simple Line Chart
Now I’ll show how to use the chart control to create a line chart in WPF. In the WpfChart project, right click the References and select Add References…to bring up the “Reference Manager” window. Click the Solution and then Projects from the left pane in this window and highlight the ChartControl. Click OK to add the ChartControl to the current project. This way, you can use the control in the WPF application just like the built-in elements. Add a new UserControl to the WpfChart project and name it ChartControlView. Here is the XAML file for this view:
<UserControl x:Class="WpfChart.ChartControlView"
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:local="clr-namespace:ChartControl;assembly=ChartControl"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="500">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="150"/>
</Grid.ColumnDefinitions>
<Button x:Name="AddChart" Content="Add Chart" Width="100" Height="25" Grid.Column="1"/>
<local:LineChart DataCollection="{Binding DataCollection}"
Xmin="0" Xmax="7" XTick="1" Ymin="-1.5" Ymax="1.5" YTick="0.5" XLabel="X" YLabel="Y"
Title="My Chart" GridlinePattern="Dot" GridlineColor="Green"/>
</Grid>
</UserControl>
Here, you simply create a line chart control exactly as you would create any other type of WPF element. Note how we specify the GridlinePattern property – simply use the Solid, Dash, Dot, or DashDot that are defined in the LinePatternEnum. This is much simpler than using code-behind, where you need to type the full path in order to define the gridlines’ line pattern. You can also specify other properties standard to WPF elements for the chart control, such as Width, Height, CanvasLeft, CanvasTop, and BackGround. These standard properties allow you to position the control, set the size of the control, or set background color of the control.
Now add a new class to the project and name it ChartControlViewModel. Here is the code for this class:
using System;
using Caliburn.Micro;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Media;
using ChartControl;
namespace WpfChart
{
[Export(typeof(IScreen)), PartCreationPolicy(CreationPolicy.NonShared)]
public class ChartControlViewModel : Screen
{
[ImportingConstructor]
public ChartControlViewModel()
{
DisplayName = "03. Chart Control";
DataCollection = new BindableCollection<LineSeriesControl>();
}
public BindableCollection<LineSeriesControl> DataCollection{get;set;}
public void AddChart()
{
DataCollection.Clear();
LineSeriesControl ds = new LineSeriesControl();
ds.LineColor = Brushes.Blue;
ds.LineThickness = 2;
ds.SeriesName = "Sine";
ds.LinePattern = LinePatternEnum.Solid;
for (int i = 0; i < 50; i++)
{
double x = i / 5.0;
double y = Math.Sin(x);
ds.LinePoints.Add(new Point(x, y));
}
DataCollection.Add(ds);
ds = new LineSeriesControl();
ds.LineColor = Brushes.Red;
ds.LineThickness = 2;
ds.SeriesName = "Cosine";
ds.LinePattern = LinePatternEnum.Dash;
for (int i = 0; i < 50; i++)
{
double x = i / 5.0;
double y = Math.Cos(x);
ds.LinePoints.Add(new Point(x, y));
}
DataCollection.Add(ds);
}
}
}
Just like we did when we created line charts previously, we need first to create the line series and then to add them to the chart control’s DataCollection. The beauty of using the chart control is that we can use the standard MVVM pattern in the application, even though we create the chart control using the dependency properties and code-behind approach. Here, we just define the DataCollection property in the view model (we do not need to implement the INotifyCollectionChanged interface for this collection because it has a built-in implementation of this interface), and bind it to the chart control. The AddChart method in the view model is also bound to the Button in the view by Caliburn.Micro’s naming convention. This way, we have a perfect separation between the view and the view model, which meets the requirement of the MVVM pattern.
Running this example and clicking the Add Chart button generate the result shown in Figure 4. You can resize the chart to see how the chart is updated automatically.
Figure 4. A line chart created using the chart control.
Creating Multiple Line Charts
Using the line chart control, you can easily create multiple charts in a single WPF window. Let’s add a new UserControl to the project and name it MultiChartView. Create a 2 by 2 Grid control and add a chart control to each of the four cells of the Grid, which can be done using the following XAML file:
<UserControl x:Class="WpfChart.MultiChartView"
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:cal="http://www.caliburnproject.org"
xmlns:local="clr-namespace:ChartControl;assembly=ChartControl"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="600">
<Grid cal:Message.Attach="[Event Loaded]=[Action AddChart]">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<local:LineChart Width="{Binding ElementName=chartGrid,Path=ActualWidth}"
Height="{Binding ElementName=chartGrid,Path=ActualHeight}"
DataCollection="{Binding DataCollection}"
Xmin="0" Xmax="7" XTick="1" Ymin="-1.5" Ymax="1.5"
YTick="0.5" XLabel="X" YLabel="Y"
Title="Chart1" GridlinePattern="Dot" GridlineColor="Black"
Grid.Column="0" Grid.Row="0"/>
<local:LineChart Width="{Binding ElementName=chartGrid,Path=ActualWidth}"
Height="{Binding ElementName=chartGrid,Path=ActualHeight}"
DataCollection="{Binding DataCollection}"
Xmin="0" Xmax="7" XTick="1" Ymin="-1.5" Ymax="1.5"
YTick="0.5" XLabel="X" YLabel="Y"
Title="Chart2" GridlinePattern="Dot" GridlineColor="Red"
Grid.Column="1" Grid.Row="0"/>
<local:LineChart Width="{Binding ElementName=chartGrid,Path=ActualWidth}"
Height="{Binding ElementName=chartGrid,Path=ActualHeight}"
DataCollection="{Binding DataCollection}"
Xmin="0" Xmax="7" XTick="1" Ymin="-1.5" Ymax="1.5"
YTick="0.5" XLabel="X" YLabel="Y"
Title="Chart3" GridlinePattern="Dot" GridlineColor="Green"
Grid.Column="0" Grid.Row="1"/>
<local:LineChart Width="{Binding ElementName=chartGrid,Path=ActualWidth}"
Height="{Binding ElementName=chartGrid,Path=ActualHeight}"
DataCollection="{Binding DataCollection}"
Xmin="0" Xmax="7" XTick="1" Ymin="-1.5" Ymax="1.5"
YTick="0.5" XLabel="X" YLabel="Y"
Title="Chart4" GridlinePattern="Dot" GridlineColor="Blue"
Grid.Column="1" Grid.Row="1"/>
</Grid>
</UserControl>
Here, we create four line chart controls, Chart1, Chart2, Chart3, and Chart4. For simplicity’s sake, in this example, we plot the same data functions on each chart. But, we set a different GridlineColor property for each chart. In practice, we can plot different math functions on each chart according to our application’s requirements.
Add a new class to the ViewModels folder and name it MultiChartViewModel. Here is the code for this class:
using System;
using Caliburn.Micro;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using ChartControl;
namespace WpfChart
{
[Export(typeof(IScreen)), PartCreationPolicy(CreationPolicy.NonShared)]
public class MultiChartViewModel : Screen
{
private readonly IEventAggregator _events;
[ImportingConstructor]
public MultiChartViewModel()
{
this._events = events;
DisplayName = "02. Multiple Charts";
DataCollection = new BindableCollection<LineSeriesControl>();
}
public BindableCollection<LineSeriesControl> DataCollection { get; set; }
public void AddChart()
{
DataCollection.Clear();
LineSeriesControl ds = new LineSeriesControl();
ds.LineColor = Brushes.Blue;
ds.LineThickness = 2;
ds.SeriesName = "Sine";
ds.LinePattern = LinePatternEnum.Solid;
for (int i = 0; i < 50; i++)
{
double x = i / 5.0;
double y = Math.Sin(x);
ds.LinePoints.Add(new Point(x, y));
}
DataCollection.Add(ds);
ds = new LineSeriesControl();
ds.LineColor = Brushes.Red;
ds.LineThickness = 2;
ds.SeriesName = "Cosine";
ds.LinePattern = LinePatternEnum.Dash;
for (int i = 0; i < 50; i++)
{
double x = i / 5.0;
double y = Math.Cos(x);
ds.LinePoints.Add(new Point(x, y));
}
DataCollection.Add(ds);
}
}
}
This view model is almost identical to that used in the preceding example where you created a single line chart. Here, the four chart controls bind to the same DataCollection object. As with any other WPF built-in element, you can place as many line chart controls as you need in a single WPF application.
Figure 5 shows the results of running this example.
Figure 5. Multiple charts created using the chart control.
Conclusion
Here, I have presented the detailed procedure on how to create a line chart control that can be used in your WPF applications with MVVM pattern. You may notice that this control lacks some features such as legend and symbol marks. If you are interested in creating fully featured 2D and 3D chart controls, please visit my website at www.DrXuDotNet.com for more information.
I have received many feedbacks from readers since I published my books Practical C# Charts and Graphics (2007), Practical WPF Graphics Programming (2007), and Practical WPF Charts and Graphics (2009). They keep asking for an updated edition. I realize the fact that the .NET technology has advanced and changed a lot in the past few years, and it is time to incorporate these new developments in .NET Framework into my book. In this new book, Practical .NET Chart Development and Applications (will be published soon), I rewrite most of the example programs in .NET 4.5 and Visual Studio 2013 to reflect .NET advancement and new programming experience I have gained as a quant developer/analyst in last few years. The key new features in this book include
- Data binding: In Windows Forms applications, data binding is mainly used for populating elements on your application with information. The beauty of data binding is that you can populate the interface while writing little to no code. With data binding in WPF you can take data from almost any property of any objects and bind it to almost any other dependency property of another object. In this book, I will try to use data binding whenever possible to implement code examples.
- Database and ADO.NET Entity Framework. For the past several years, I have worked with a financial firm as a quant analyst/developer on Wall Street. The most important thing I deal with every day is the market data. Most .NET applications in different fields also need to interact with data stored in databases. Therefore, this book includes a chapter that deals with database and ADO.NET entity Framework. It shows you how to create a simple database and how to use the entity data model to access the database data.
- MVVM pattern: In traditional UI development, you create a view using window or user control and then write all logical code in the code behind. This approach creates a strong dependency between UI and data logic, which is hard to maintain and test. On the other hand, in MVVM, the glue code is the view model. If property values in the view model change, those new values automatically propagate to the view via data binding and notification. In this book, I will introduce the MVVM pattern and try to use the view model for data binding.
- Powerful Chart Controls: In this new book, I convert 2D line charts, stock charts, and 3D charts into powerful chart controls that you can easily reuse in your .NET applications. In particular, these chart controls are MVVM compatible and allow you to develop .NET applications with 2D and 3D charts based on MVVM pattern.
- Financial Market: This new book incorporates more topics and examples in financial market, mainly from my own working experience, including interaction with market data, moving average calculation, linear regression, principal component analysis (PCA) for pair trading, retrieving market data from stock charts, and implementing reusable stock chart controls.
- Real-time Charts: Many fields require real-time chart capability. For example, if you design a stock trading system, you need to develop a real-time data feeder and a chart control to display the real-time stock market data on your screen. This new book provides some examples that show how to create such real-time chart applications using our reusable chart controls.
Please visit my website at www.DrxuDotNet.com for more information.