Introduction
There are tons of articles on the MVVM (Model-View-ViewModel) design pattern, and rightfully so. It's an easy pattern to understand (after you've studied it for a while) and it very nicely separates concerns. Where better to separate concerns than with a Chart and its data? This article shows you how to use the .NET Framework's (free) Datavisualization Chart component using the MVVM model.
Background
This sample was built using Visual Studio 2010 running .NET Framework 4.0. It also uses the WPF Toolkit version 3.5 which has the System.Windows.Controls.DataVisualization
namespace. You can download this toolkit from Codeplex here. This solution is new to CodeProject as the previous examples of the DataVisualization
namespace were either all for ASPX Charting or WinForms Charting. This particular project is for WPF and includes the entire current set of ChartTypes
found in the Datavisualization
Namespace.
Using the Code
The zip file for this article (minus the DataVisualization
component) is an entire project that allows you to compile and run a sample of every chart type currently supported in WPF. Simply run the project in debug mode, once it comes up, just change the SeriesTypes
as shown below on the left, to change the view. Notice below that the Title, Legend Title are also editable. This allows one to see how it was done and add other editable attributes to the Charting component.
Points of Interest
The View
Views in the MVVM pattern are just the markup in XAML needed for displaying content and style. Be careful when putting code function in the "code-behind" of the View. One typically wants to limit that code to View Only logic such as Animation. The content of the View is handled in the ViewModel via WPF Binding. The MainWindow
("Shell") XAML is shown below:
<Window
x:Class="WPFCharting.Views.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cmd="clr-namespace:WPFCharting.Commands"
xmlns:ViewModels="clr-namespace:WPFCharting.ViewModels"
xmlns:Views="clr-namespace:WPFCharting.Views"
Title="MainWindowView"
MinHeight="400"
MinWidth="800"
Height="Auto" Width="Auto">
<Window.Resources>
<ViewModels:MainWindowVM x:Key="VM"></ViewModels:MainWindowVM>
<cmd:ShowAlterData x:Key="SAD"></cmd:ShowAlterData>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="21*"></ColumnDefinition>
<ColumnDefinition Width="80*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Border Padding="2" >
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,.7">
<GradientStop Color="White" Offset="0" />
<GradientStop Color="Black" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<Border Padding="1" >
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Black" Offset="0" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel Grid.Column="0" Margin="5,5,5,0"
DataContext="{Binding Source={StaticResource VM}}">
<StackPanel.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Black" Offset="0" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush>
</StackPanel.Background>
<Views:UcTitle x:Name="XTitle" ></Views:UcTitle>
<Views:UcLegendTitle x:Name="XLegendTitle"/>
<Views:UcSeriesTitle x:Name="XSeriesTitle" />
<Views:UcSeriesTypes x:Name="XSeriesTypes"/>
<Border Padding="2,5,2,5" Background="Black">
<StackPanel>
<Label Foreground="LightBlue"
HorizontalContentAlignment="Center">Edit</Label>
<Button Command="{Binding Source={StaticResource SAD}}">
Show/Alter Data</Button>
<Button>Reset To Original</Button>
</StackPanel>
</Border>
</StackPanel>
</Border>
</Border>
<ContentControl Grid.Column="1"
DataContext="{Binding Source={StaticResource VM}}"
Content="{Binding Source={StaticResource VM}, Path=MainChart}" />
</Grid>
<Window.Background>
<LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
<GradientStop Color="Black" Offset="0.016" />
<GradientStop Color="White" Offset="0" />
<GradientStop Color="Black" Offset="0.205" />
<GradientStop Color="Black" Offset="0.079" />
<GradientStop Color="White" Offset="0.212" />
</LinearGradientBrush>
</Window.Background>
</Window>
Notice there's really not much there, as it is really just a "Shell". There are just two "ContentControls
" in the Shell, one is the StackPanel
containing the Left side Editing UserControl
s and the other is the Content Control which "houses" the charts. One note of interest here is that the DataContext
of the ChartControl
had to be set explicitly as shown (as well as the Content and the Path) statements. Setting the DataContext
anywhere else in this project failed. For example, it didn't work in the Grid
. Perhaps I misspelled something, but it worked in the ContentControl
so I left it there.
The ViewModel
which contains (among other things) property getters and setters has a number of private
/public
properties which are for the WPF MainWindow
(Shell). The call to PropChanged
is how the ViewModel
notifies the View
. Those property getters/setters take on a look like this:
private ContentControl _mainChart;
public ContentControl MainChart {
get { return _mainChart; }
set {
PreviousViewList.Add(_mainChart);
_mainChart = value;
PropChanged("MainChart");
}
}
So in the code above, we see the "pattern" of MVVM property "getters/setters". The private
variable encapsulates the public get
and set
methods, the set
method will then update the private
variable and call PropChanged
.
One question that arises is, how do we boot up a MVVM application. In this solution, you will find it being done in the App.cs file using this code:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
MainWindow mw = new MainWindow();
mw.Show();
}
The OnStartup
event is overridden and the MainWindow
is instantiated and shown. From there, the Main Window will instantiate the MainWindowViewModel
as shown next. The MainWindow
is shown, but how do we start the MainWindowViewModel
?
In the MainWindow, we see the MainWindowViewModel's class instantiation happening via XAML. This is done in the Window.Resouces
section of the MainWindow.Xaml file. The MVVM pattern states "The View knows the View-Model, and the View-Model knows the Model".
<Window.Resources>
<local:MainWindowVM x:Key="VM"></local:MainWindowVM>
</Window.Resources>
Note however that Binding between the View
and the Viewmodel
is done via the WPF Binding system so that when a project is run, the getter methods are called at View.Show()
time. It (the ViewModel) then becomes a static
object, which the charting Series UserControls (of this project) use to bind data. We gave it a name of "VM" for View Model as shown above.
Static Resources and WPF Binding is not a topic for this article; however, it pays to know how they work because certain aspects of the MVVM pattern use XAML based resources as shown above. You can read these articles on MVVM Resources.
Ahh! And now for the Markup that controls which type of ChartSeries
is shown. Each of the UserControls
in this project represent a different ChartView
(Such as this Area control User Control). For Charting, a chart markup in XAML can contain a type of Chart Series. In this case (see Markup below), the PieSeries
chart type is shown. NOTE: What is lacking in the Charting Framework; however, is an Generic Series object of which to bind to at design time. For example, there is no ChartingToolKit:Series generic type which would allow chart series types to be injected at run time changing the views of data. So as an alternative to this limitation, this project has 1 UserControl
per chart type. This was the only way known to the author to do this in XAML.
<chartingToolkit:LineSeriesx:Name="XPieSeries"
DataContext="{Binding}"
DependentValueBinding="{Binding Path=Value}"
IndependentValueBinding="{Binding Path=Key}"
ItemsSource="{Binding Path=data}"/>
If you are experimenting with Chart Series types (see XAML above), make sure to set up the Dependent and Independent Value Bindings to the correct Paths. This author lost a considerable amount of time with charts not showing up simply because the Value, and Key paths were bound to the wrong Axis. Notice also the generic Binding being used, there is no Source specified in these UserControl
s. This is due to WPFs nice DataContext
inheritance effect. The MainWindowView
sets the DataContext
, so no explicit DataContext
statements are needed in these UserControls
which inherit this DataContext
.
In the most current iteration of this project, there are 7 different UserControls
found in the Views folders which are used to swap the content of the MainWindow
Shell. These UserControls
are the charttype implementations e.g. BarSeries
, PieSeries
, ColumnSeries
, etc. Driven by the command pattern of the MVVM architecture, each of these classes use a static getInstance()
getter, setter method to ensure the fastest possible swapping of the Chart Views.
You will find the implementation of the Command to swap the views in the Command folder. It is just a wrapper class invokable from the MainWindow
View. This class "ShowSeries
" is simply a select
statement that gets the current instance of the Charttype
. It then injects the Chart view into the MainWindow
. While this is not a formalized IOC or Dependency Injection pattern, it simulates the same thing. In particular, adding the concept of Singleton Pattern (getInstance
) for the sake of speed. BTW you'll find these Views are able to be injected rather fast due to the Singleton getInstance
method.
The View Model
So what's easier than using an ObservableCollection
of something when working with WPF? It's a wrapper class made just for exposing data to the View
. What's really nice is that it is a generic collection that allows you to define the type (to be displayed). These three lines of code is all you need to wire-up a Chart in the View Model. The code in the zip file will show you how to notify the WPF layer of any changes.
private ObservableCollection<KeyValuePair<string, int>> _data;
public ObservableCollection<KeyValuePair<string,int>> data {
get { return _data; }
}
The XAML above shows how the ViewModel
delivers the data to the chart.
On the left hand side of the MainWindow
, there is a "controller" that allows you to edit content within the Chart itself. It uses the ViewModel
as does the UserControls
(ChartTypes
) to get
and set
the properties. This is an instance whereby multiple controls are all bound to the same ViewModel
and demonstrates how the ViewModel
can host separate concerns. Imagine a Webbrowser
application that has navigation links on the left. The ViewModel
can actually serve multiple ContentControls
at the same time, without violating Separation of Concerns. However, keep in mind that the adage of "One Time and One Place" an "Each Object is responsible for itself" will guide you in determining how much is too much code within one class. This is why the Command
interface is nice because it moves implementation out of the ViewModel
. In this example, the Commands
back-end the setter properties of the View Model.
The Model
Finally, we come to the point where we implement the Data Layer in the Model for the class. Nothing more than a concrete class of Observable
collection of a KeyValuePair<string,int>
type:
public class MainWindowModel : ObservableCollection<KeyValuePair<string, int>>
{
public MainWindowModel()
{
init();
}
public void init()
{
Add(new KeyValuePair<string, int>("Dog", 30));
Add(new KeyValuePair<string, int>("Cat", 25));
Add(new KeyValuePair<string, int>("Rat", 5));
Add(new KeyValuePair<string, int>("Hampster", 8));
Add(new KeyValuePair<string, int>("Rabbit", 12));
}
public ObservableCollection<KeyValuePair<string, int>> getData()
{
return this;
}
}
Voila! That's all there is to this. Now imagine that you want to have SQL Connections or XML Readers providing content. As you can see, you have many options. You can create new methods within this class above, or you can create other back-end classes that update this class. The good news is that if you are able to follow this contract of a KeyValuePair<string,int>
then the implementation is hidden from the View
. You could have many different getData
methods in this class.
History
- 4th Update: Fixed Links in main article 10/19/2010
- 3rd Update: Clarified the View's Code Behind Statement, added links to Browse Code. Clarified other content. 10/15/2010
- 2nd Edition: Included all Chart Types, MVVM Commanding, View Injection as well as good examples of Binding, to show how to change content in a chart after it shows data. Lots of comments in the code. 10/12/2010 XZZ0195
- Kenny Rogers and the 1st Edition: 10/12/2010 XZZ0195