使用 MVVM 模式的 WPF 图表
展示了如何使用 MVVM 模式在 .NET 4.0 中实现 WPF 图表
引言
关于 MVVM(Model-View-ViewModel)设计模式的文章汗牛充栋,而且理所当然。它是一个容易理解的模式(在你研究了一段时间之后),并且能非常巧妙地分离关注点。还有哪里比图表及其数据更适合分离关注点呢?本文将向您展示如何使用.NET Framework 的(免费)数据可视化图表组件,结合 MVVM 模型。
背景
此示例使用 Visual Studio 2010 和 .NET Framework 4.0 构建。它还使用了 WPF Toolkit 版本 3.5,其中包含 System.Windows.Controls.DataVisualization
命名空间。您可以在此处从 Codeplex 下载此工具包。这个解决方案对 CodeProject 来说是新的,因为以前关于 DataVisualization
命名空间的示例要么是用于 ASPX 图表,要么是用于 WinForms 图表。这个项目特别适用于WPF,并且包含了 Datavisualization
命名空间中当前所有 ChartTypes
。
Using the Code
本文的 zip 文件(不含 DataVisualization
组件)是一个完整的项目,允许您编译和运行 WPF 支持的每种图表类型的示例。只需在调试模式下运行项目,一旦项目启动,只需更改左侧所示的 SeriesTypes
,即可更改视图。请注意,下方所示的标题和图例标题也是可编辑的。这使得人们可以了解其实现方式,并将其他可编辑属性添加到图表组件中。

关注点
View
在 MVVM 模式中,视图(Views)仅仅是 XAML 中用于显示内容和样式的标记。请注意不要在视图的“代码隐藏”(code-behind)中放入功能代码。通常,应将代码限制在仅与视图相关的逻辑,例如动画。视图的内容由 ViewModel 通过 WPF 绑定处理。MainWindow
(“Shell”)XAML 如下所示。
<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>
请注意,这里的内容不多,因为它确实只是一个“Shell”。Shell 中只有两个“ContentControls
”,一个是包含左侧编辑 UserControls
的 StackPanel
,另一个是“容纳”图表的 Content Control
。这里有一个值得注意的地方是,ChartControl
的 DataContext
必须像所示那样显式设置(以及 Content
和 Path
语句)。在此项目中的其他任何位置设置 DataContext
都会失败。例如,在 Grid
中不起作用。也许是我拼写错误,但它在 ContentControl
中起作用,所以我把它留在了那里。
ViewModel
包含(除其他外)属性的 getter 和 setter,它有许多 private
/public
属性用于 WPF MainWindow
(Shell)。调用 PropChanged
是 ViewModel
通知 View
的方式。这些属性的 getter/setter 看起来像这样:
private ContentControl _mainChart;
/// <summary>
/// When swapping ChartTypes this is the content which is injected to view
/// </summary>
public ContentControl MainChart {
get { return _mainChart; }
set {
PreviousViewList.Add(_mainChart);
_mainChart = value;
PropChanged("MainChart");
}
}
因此,在上面的代码中,我们看到了 MVVM 属性“getter/setter”的“模式”。private
变量封装了 public get
和 set
方法,set
方法将更新 private
变量并调用 PropChanged
。
一个会引起的问题是,我们如何启动一个 MVVM 应用程序。在此解决方案中,您会在 App.cs 文件中找到使用以下代码实现的启动方式:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
MainWindow mw = new MainWindow();
mw.Show();
}
重写了 OnStartup
事件,并实例化并显示了 MainWindow
。从那里开始,主窗口将实例化 MainWindowViewModel
,如下所示。MainWindow
被显示,但我们如何启动 MainWindowViewModel
呢?
在MainWindow 中,我们看到 MainWindowViewModel
类的实例化是通过 XAML 完成的。这在 MainWindow.Xaml 文件的 Window.Resouces
部分完成。MVVM 模式指出:“视图了解 ViewModel,ViewModel 了解模型”。
<Window.Resources>
<local:MainWindowVM x:Key="VM"></local:MainWindowVM>
</Window.Resources>
但请注意,视图和 ViewModel 之间的绑定是通过 WPF 绑定系统完成的,因此当项目运行时,getter 方法在 View.Show()
时被调用。(此项目的)ViewModel 随后成为一个static
对象,图表系列 UserControls 使用它来绑定数据。我们将其命名为“VM”,代表 View Model,如上所示。
静态资源和 WPF 绑定不是本文的主题;然而,了解它们的工作原理很有价值,因为 MVVM 模式的某些方面使用了基于 XAML 的资源,如上所示。您可以阅读关于 MVVM 资源的这些文章。
啊!现在轮到控制显示哪种 ChartSeries
类型的标记了。本项目中的每个 UserControl
都代表一个不同的 ChartView
(例如这个 Area 控件 User Control)。对于图表,XAML 中的图表标记可以包含一种图表系列。在这种情况下(见下面的标记),显示的是 PieSeries
图表类型。注意:然而,在图表框架中,缺乏一个通用的 Series 对象,可以在设计时绑定。例如,没有 ChartingToolKit:Series 通用类型允许在运行时注入图表系列类型来改变数据视图。 因此,作为此限制的替代方案,本项目为每个图表类型提供一个 UserControl
。据作者所知,这是在 XAML 中实现此功能的唯一方法。
<chartingToolkit:LineSeriesx:Name="XPieSeries"
DataContext="{Binding}"
DependentValueBinding="{Binding Path=Value}"
IndependentValueBinding="{Binding Path=Key}"
ItemsSource="{Binding Path=data}"/>
如果您正在尝试不同的图表系列类型(参见上面的 XAML),请务必将依赖值和独立值绑定设置到正确的路径。作者花费了大量时间,因为图表未显示,仅仅是因为 Value 和 Key 路径被绑定到了错误的轴。另请注意所使用的通用绑定,在这些 UserControls
中没有指定 Source。这是因为 WPF 的 DataContext
继承效果很好。MainWindowView
设置了 DataContext
,因此在这些继承了 DataContext
的 UserControls
中不需要显式的 DataContext
语句。
在此项目的最新迭代中,Views 文件夹中共有 7 个不同的 UserControls
,用于交换 MainWindow
Shell 的内容。这些 UserControls
是图表类型的实现,例如 BarSeries
、PieSeries
、ColumnSeries
等。通过 MVVM 架构的命令模式驱动,这些类中的每一个都使用 static getInstance()
getter、setter 方法,以确保最快的图表视图切换。
您将在 Command 文件夹中找到命令的实现,用于切换视图。它只是一个可以从 MainWindow
视图调用的包装器类。这个类“ShowSeries
”只是一个 select
语句,它获取 Charttype
的当前实例。然后,它将图表视图注入 MainWindow
。虽然这不是正式的 IOC 或依赖注入模式,但它模拟了相同的功能。特别是,为了提高速度,添加了 Singleton 模式(getInstance
)的概念。顺便说一句,您会发现由于 Singleton getInstance
方法,这些视图可以非常快速地注入。
ViewModel
使用 WPF 时,有什么比使用 ObservableCollection
更简单?它是一个专门用于将数据暴露给 View
的包装器类。真正好的一点是,它是一个泛型集合,允许您定义要显示的类型。这三行代码就足以在 ViewModel 中连接一个图表。zip 文件中的代码将向您展示如何向 WPF 层通知任何更改。
private ObservableCollection<KeyValuePair<string, int>> _data;
public ObservableCollection<KeyValuePair<string,int>> data {
get { return _data; }
}
上面的 XAML 显示了 ViewModel
如何将数据交付给图表。
在 MainWindow
的左侧,有一个“控制器”,允许您编辑图表本身的内容。它像 UserControls
(ChartTypes
)一样使用 ViewModel
来 get
和 set
属性。这是一个多个控件都绑定到同一个 ViewModel
的实例,并演示了 ViewModel
如何托管分离的关注点。想象一个 Webbrowser
应用程序,左侧有导航链接。ViewModel
实际上可以同时服务于多个 ContentControls
,而不会违反关注点分离。但是,请记住,“一次只在一个地方”和“每个对象对自己负责”的格言将指导您确定一个类中有多少代码是过多的。这就是为什么 Command
接口很有用的原因,因为它将实现从 ViewModel
中移出。在此示例中,Commands
是 ViewModel 的 setter 属性的后端。
模型
最后,我们来实现 Model 中类的 Data Layer。它只是一个 Observable
集合的具体类,类型为 KeyValuePair<string,int>
。
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;
}
}
Voilà!就是这样了。现在,想象一下您想使用 SQL 连接或 XML 读取器提供内容。如您所见,您有很多选择。您可以在上面的类中创建新方法,或者创建其他后端类来更新此类。好消息是,如果您能遵循 KeyValuePair<string,int>
的契约,那么实现对 View
来说就是隐藏的。您可以在这个类中有许多不同的 getData
方法。
历史
- 第 4 次更新:修复了主文章中的链接 2010/10/19
- 第 3 次更新:澄清了视图的代码隐藏声明,添加了浏览代码的链接。澄清了其他内容。2010/10/15
- 第二版:包含所有图表类型、MVVM 命令、视图注入以及良好的绑定示例,以展示如何在图表显示数据后更改其内容。代码中包含大量注释。2010/10/12 XZZ0195
- Kenny Rogers 和第一版:2010/10/12 XZZ0195