MVVM介绍

MVVM是XAML程序开发中常用的设计模式,能够实现用户界面和数据、业务逻辑的完全分离。
为实现分离MVVM将程序分割为以下三个类别。

  • Model - 程序中使用的数据模型。
  • View - 用户界面。
  • ViewModel-处理Model中的数据并为View提供业务逻辑。

理想的MVVM程序中,组件必须完全包含在这三个类别中。比如XAML页面中不应包含代码,同样所有与视图不直接相关的代码都必须由ViewModel提供。数据绑定提供了支持在WPF中,Windows商店和Windows Phone应用程序允许使用XAML定义UI和将控件与视图ViewModel类所需的数据和功能绑定。

一般来说,使用MVVM创建程序时,我们的大部分时间都花在ViewModel上。这些类提供提供界面使用的数据。ViewModel也可能包含处理用户界面事件和其他View需要的业务逻辑。

MVVM模式文档很完备。该教程不是为讲述MVVM模式,而是介绍在其ArcGisRun Runtime SDK for .NET开发中的使用。要了解更多关于MVVM的内容,请参阅文末链接。

先决条件

该教程需要Microsoft Visual Studio和 ArcGIS Runtime SDK for .NET.
SDK安装和系统需求请参阅以下链接installing the SDK and system requirements.

需要了解 Visual Studio, XAML, and C# 。

创建WPF程序

使用Visual Studio创建一个WPF程序。

  • 打开Visual Studio。
  • 选择文件>新建>项目创建一个新的项目。
  • 在新建项目界面选择Windows Desktop > WPF 程序

提示:
ArcGIS Runtime SDK for .NET 提供了项目模板:ArcGIS Runtime 10.2.7 for .NET App
使用模板创建项目会自动添加引用和带有mapvew的主页面。

  • 选择项目路径修改项目名称为MvvmApp。
  • 点击确定创建项目。
  • 右击项目下的引用添加Esri.ArcGISRuntime引用。

Esri.ArcGISRuntime.dll包含mapcontrol和要用到的所有核心API。

添加Map

本程序将加载ArcGIS Online 的影像底图和一个点图层。定义点的渲染方式和当前的视图范围。

在XAML designer中使用ArcGISRuntime需要添加命名空间。

  • 在设计器中打开主页面MainWindow.xaml.
  • 添加以下命名空间
1
xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
  • 标签下添加以下xaml代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<esri:MapView x:Name="MyMapView">
<esri:Map>
<esri:Map.InitialViewpoint>
<esri:ViewpointExtent XMin="-1631122.453" YMin="4253523.549" XMax="4163264.136" YMax="8976345.495" />
</esri:Map.InitialViewpoint>
<esri:ArcGISTiledMapServiceLayer ID="BaseMap"
ServiceUri="http://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"/>
<esri:FeatureLayer ID="Incidents">
<esri:ServiceFeatureTable
ServiceUri="http://sampleserver6.arcgisonline.com/arcgis/rest/services/SF311/FeatureServer/0"/>
<esri:FeatureLayer.Renderer>
<esri:SimpleRenderer>
<esri:SimpleMarkerSymbol Color="Red" Size="16" Style="Triangle"/>
</esri:SimpleRenderer>
</esri:FeatureLayer.Renderer>
</esri:FeatureLayer>
<esri:GraphicsLayer ID="PointGraphics"/>
</esri:Map>
</esri:MapView>
  • 编译运行界面如下图。

移除地图数据

为了将界面与数据和逻辑分离,我们移除mapview中的map。将其存储在程序的资源字典中,并使用数据绑定显示到界面。

  • 选择esri:MapView标签下的所有内容》剪切。
  • 打开App.xaml文件。
  • 将剪切的代码粘贴到<Application.Resources>标签下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<Application.Resources>
<esri:Map>
<esri:Map.InitialViewpoint>
<esri:ViewpointExtent XMin="-1631122.453"
YMin="4253523.549"
XMax="4163264.136"
YMax="8976345.495" />
</esri:Map.InitialViewpoint>
<esri:ArcGISTiledMapServiceLayer ID="BaseMap"
ServiceUri="http://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"/>
<esri:FeatureLayer ID="Incidents">
<esri:ServiceFeatureTable
ServiceUri="http://sampleserver6.arcgisonline.com/arcgis/rest/services/SF311/FeatureServer/0"/>
<esri:FeatureLayer.Renderer>
<esri:SimpleRenderer>
<esri:SimpleMarkerSymbol Color="Red" Size="16" Style="Triangle"/>
</esri:SimpleRenderer>
</esri:FeatureLayer.Renderer>
</esri:FeatureLayer>
<esri:GraphicsLayer ID="PointGraphics"/>
</esri:Map>
</Application.Resources>
  • 添加ArcgisRuntime命名空间。
  • 给map添加x:Key属性并赋值IncidentMap。
1
<esri:Map x:Key="IncidentMap">

x:Key 用与识别资源,作用范围内值应唯一

  • 返回主页面添加代码将IncidentMap资源绑定到mapview
1
2
<esri:MapView x:Name="MyMapView" Map="{Binding Source={StaticResource IncidentMap}}">           
</esri:MapView>

地图属性的数据绑定使用绑定标记扩展在XAML中指定。可以绑定应用程序中的资源,页面上的其他元素,或应用程序中提供数据的类。在XAML应用程序中对数据绑定的更多信息可以参阅数据绑定概述(MSDN).aspx)。

  • 编译运行,程序与之前相同。

我们的程序现在实现MVVM模式了吗?还不是最理想的形式,app.xaml页面作为一个视图模型提供了一个可以绑定的地图。这说明使用该模式的优势之一,它清楚地将UI与实现细节分开。MapView是UI控件,属于页面。它包含的地图和图层是控件中显示的数据,该数据需要更改,并且应在UI之外进行管理。有了这个架构,页面和地图是松散耦合的,这意味着你可以很容易地改变一个不影响另一个。若要更改页中显示的地图,只需将绑定指向应用程序中可用的其他资源。

为实现传统形式的MVVM模式,我们需要创建一个ViewModel类来充当页面的数据上下文,并公开用户界面可以绑定的数据和功能。

创建一个ViewModel类

ViewModel提供视图可以通过数据绑定访问的数据和功能。一个视图对应一个视图模型是很常见的,但是多个些视图模型与单个视图关联并不多见。我们也可以有一个视图模型,该模型被应用程序中的几个不同视图使用。我们将创建一个单一的视图模型(mapviewmodel)提供的所有页面需要的数据和功能。

  • 右击项目》添加类:MapViewModel.cs.
  • 添加引用
1
2
using Esri.ArcGISRuntime.Controls;
using Esri.ArcGISRuntime.Layers;
  • 添加新的属性:IncidentMap。
1
2
3
4
5
6
private Map map;
public Map IncidentMap
{
get { return this.map; }
set { this.map = value; }
}
  • IncidentMap属性提供视图需要绑定的数据。
  • 添加构造函数、初始化地图
1
2
3
4
5
public MapViewModel()
{
// when the view model initializes, read the map from the App.xaml resources
this.map = App.Current.Resources["IncidentMap"] as Map;
}

绑定ViewModel到View

为了关联ViewModel和View,我们需要将View的DataContext属性设置为ViewModel的一个实例。可以使用xaml或view的codebehind设置。以下步骤使用xaml将viewmodel中的incidentmap绑定到view中mapview的map属性

  • 打开app.xaml。添加本地程序集xaml命名空间
1
2
3
4
5
6
<Application x:Class="MvvmApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
xmlns:local="clr-namespace:MvvmApp"
StartupUri="MainWindow.xaml">
  • 在Application.Resources 标签IncidentMap定义后添加xanl定义一个MapViewModel对象并赋值X:key为MapVM
1
<local:MapViewModel x:Key="MapVM"/>
  • 打开MainWindow.xaml 设置整个页面的DataContext绑定MapVM对象。
1
2
3
4
5
6
<Window x:Class="MvvmApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding Source={StaticResource MapVM}}">

一旦ViewModel设置为整个页面的DataContext,页面上的任一控件都可绑定ViewModel暴露出的属性。

  • 修改mapview的绑定声明。
1
2
<esri:MapView x:Name="MyMapView" Map="{Binding IncidentMap}">           
</esri:MapView>
  • 编译运行,地图和在页面中直接定义时相同。

使用这种模式显示地图的优势是什么?无论我们在哪里定义地图,应用程序看起来都是一样的,但使用ViewModel似乎增加了很多复杂性。对于这样一个简单的应用程序,使用MVVM模式的确没有优势。但当应用程序变得更加复杂时,你会发现MVVM架构使应用程序更容易维护并促进应用程序之间共享代码。

在ViewModel中处理点击事件

在视图模型中保存数据并将其绑定到UI很简单,但是关于视图函数和功能呢?

如果完全实现MVVM模式,所有的业务逻辑都应包含在ViewModel中。页面的codebehind也不应包含事件代码。幸运的是,数据绑定还可以将View中的控件事件绑定到ViewModel中的代码。

执行Command

一些控件,如按钮、复选框、单选按钮和菜单项,提供了一个Command属性。Command是自定义类,实现ICommand接口。定义当一个控件点击时的事件(执行方法),并确定何时应该启用(执行方法)。可以在视图模型中创建Command,并绑定到相应控件的Command属性。

提示:
现在有很多MVVM的框架提供了ICommand的实现。使用这些框架时,我们可以在视图模型中实例化命令对象,而不必创建本节所描述的自己的命令类的中间步骤。如果计划广泛使用MVVM,可以考虑使用框架开发应用程序,如MVVMLight

  • 新建类并命名DelegateCommand.cs。

  • 添加命名空间System.Windows.Input

1
using System.Windows.Input;
  • 在 DelegateCommand类中实现Icommand接口。完整的实现代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class DelegateCommand : System.Windows.Input.ICommand
{
// a var to store the command's execute logic (button click, for example)
private readonly Action<object> execute;

// a var to store the command's logic for enabling/disabling
private readonly Func<object, bool> canExecute;

// an event for when the value of "CanExecute" changes (not implemented)
public event EventHandler CanExecuteChanged;

// constructor: store the logic for executing and enabling the command
public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecuteFunc = null)
{
this.canExecute = canExecuteFunc;
this.execute = executeAction;
}

// if it was passed in, execute the enabling logic for the command
public bool CanExecute(object parameter)
{
if (this.canExecute == null) { return true; }

return this.canExecute(parameter);
}

// execute the command logic
public void Execute(object parameter)
{
this.execute(parameter);
}
}

当需要绑定一个按钮和命令时,创建一个delegatecommand实例传递事件命令的执行和控件到构造函数。执行操作的参数是object类型,以给命令提供最大的灵活性。

  • 保存和关闭DelegateCommand.cs。

在ViewModel中创建Command属性

任何需要绑定的内容都必须是public的属性。下一步我们将创建MapViewModel的一个新属性返回一个DelegateCommand 对象。该命令被定义为执行和启用逻辑,因此它可以绑定到视图中的按钮。

  • 打开MapViewModel类定义一个public属性:ToggleLayerCommand 返回一个DelegateCommand对象。
1
public DelegateCommand ToggleLayerCommand { get; set; }
  • 创建一个新的函数响应命令执行,命名togglelayer。该函数控制map中图层的可见性。输入参数指定要修改的图层名称。
1
2
3
4
5
6
private void ToggleLayer(object parameter)
{
var lyr = this.map.Layers[parameter.ToString()];
lyr.IsVisible = !(lyr.IsVisible);

}
  • 创建另一个新的函数来确定Command的执行状态。由于命令修改一个特定的图层,只有当图层存在时才可执行。参数与上一个函数相同。
1
2
3
4
5
private bool OkToExecute(object parameter)
{
var lyr = this.map.Layers[parameter.ToString()] as FeatureLayer;
return (lyr != null);
}

如果指定名称的图层不存在,oktoexecute返回false,禁用相关的控件。

  • 在mapviewmodel的构造函数,添加以下代码行来实例化togglelayercommand。传递thetogglelayer函数作为命令的执行逻辑、oktoexecute为enable逻辑。
1
2
3
4
5
6
public MapViewModel()
{
// when the view model initializes, read the map from the App.xaml resources
this.map = MvvmApp.App.Current.Resources["IncidentMap"] as Map;
ToggleLayerCommand = new DelegateCommand(ToggleLayer, OkToExecute);
}

##绑定一个button到ViewModel的Command

按钮提供一个可执行代码的单击事件。指定一个Command定义按钮单击的代码,它的优点是还包含了指示按钮何时启用或禁用的逻辑。一个按钮对象的Command属性都可以绑定到实现ICommand接口的对象。

  • 在MainWindow.xaml 页面添加一个新的button
1
2
3
<Button Height="30" Width="70" 
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Content="Toggle" />
  • 设置button的Command属性绑定到view model的ToggleLayerCommand属性。
1
2
3
4
<Button Height="30" Width="70" 
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Content="Toggle"
Command="{Binding ToggleLayerCommand}"/>
  • 设置按钮的commandparameter属性值,为Command提供参数。
1
2
3
4
5
<Button Height="30" Width="70" 
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Content="Toggle"
Command="{Binding ToggleLayerCommand}"
CommandParameter="Incidents"/>
  • 编译运行,点击Toggle按钮控制incidents图层的可见性。

在ViewModel中处理其他事件

前面描述的视图模型中绑定命令的过程适用于提供所需命令属性的控件。如果需要处理其他不能直接使用命令属性绑定的事件,该怎么办?幸运的是,.NET提供了额外的类,可以将视图中的事件绑定到视图模型中的命令或函数。

在下面的步骤中,将使用system.windows.interactivity的类来处理MapView的ExtentChanged事件。

  • 选择**项目》添加引用,添加System.Windows.Interactivity引用

提示:
如果引用不存在需要安装Microsoft Expression Blend SDK.或在NuGet中搜索“blend”下载。

  • 打开MainWindow.xaml在标签内添加以下xaml命名空间。
1
xmlns:interactivity="http://schemas.microsoft.com/expression/2010/interactivity"
  • MapView 标签下添加以下xaml定义ExtentChanged事件的触发器。
1
2
3
4
5
6
7
<esri:MapView x:Name="MyMapView" Map="{Binding IncidentMap}">
<interactivity:Interaction.Triggers>
<interactivity:EventTrigger EventName="ExtentChanged">

</interactivity:EventTrigger>
</interactivity:Interaction.Triggers>
</esri:MapView>

可以使用一个函数(方法)或一个命令对象来响应事件。因为需要一个参数(MapView),所以使用command并提供一个CommandParameter将当前视图范围传递到view model

  • 添加以下xaml定义InvokeCommandAction响应事件。ViewModel中还不存在ExtentChangedCommand,我们稍后创建。
1
2
3
<interactivity:InvokeCommandAction 
Command="{Binding ExtentChangedCommand}"
CommandParameter="{Binding ElementName=MyMapView}"/>
  • 打开MapViewModel.cs添加以下代码定义一个DelegateCommand对象并命名为ExtentChangedCommand
1
public DelegateCommand ExtentChangedCommand { get; set; }
  • 添加以下函数响应视图范围改变。现在需要确保能获取到 mapview的视图范围。
1
2
3
4
5
6
public void MyMapViewExtentChanged(object parameter)
{
var mv = parameter as MapView;
var extent = mv.Extent;

}
  • 在构造函数中添加以下代码实例化ExtentChangedCommand.
1
2
3
4
5
6
7
public MapViewModel()
{
// when the view model initializes, read the map from the App.xaml resources
this.map = MvvmApp.App.Current.Resources["IncidentMap"] as Map;
ToggleLayerCommand = new DelegateCommand(ToggleLayer, OkToExecute);
ExtentChangedCommand = new DelegateCommand(MyMapViewExtentChanged);
}

DelegateCommand中的是否可执行逻辑是可选的,所以我们没有指定它。

  • 在MyMapViewExtentChanged函数最后一行添加断点,运行程序,看是否命中断点查看获取的mapview范围。测试完毕后移除断点。

至此我们已经完成了在ViewModel中处理视图范围更改事件的绑定。以上过程可以用来处理所有界面控件的事件,包括不提供Command属性的控件。

绑定视图的范围值

要在应用程序中显示当前范围的坐标,需要在ViewModel上创建新公共属性以公开该信息。然后可以将属性绑定到用户界面元素,如文本框。

  • 打开MapViewModel.cs添加以下代码定义一个CurrentExtentString属性。
1
2
3
4
5
6
7
8
9
10
11
12
private string extentString;
public string CurrentExtentString
{
get
{
return this.extentString;
}
set
{
this.extentString = value;
}
}
  • 在MyMapViewExtentChanged中添加以下代码设置CurrentExtentString的值
1
2
3
4
5
6
7
public void MyMapViewExtentChanged(object parameter)
{
var mv = parameter as MapView;
var extent = mv.Extent;
CurrentExtentString = string.Format("XMin={0:F2} YMin={1:F2} XMax={2:F2} YMax={3:F2}",
extent.XMin, extent.YMin, extent.XMax, extent.YMax);
}

视图范围描述格式如下:

1
XMin=-2598746.47 YMin=4253523.55 XMax=5130888.16 YMax=8976345.50.
  • 打开MainWindow.xaml在mapview下添加以下xaml定义一个TextBlock用来显示当前试图范围。
1
2
3
4
<TextBlock Height="30" Width="Auto" 
FontSize="16" Foreground="AliceBlue"
HorizontalAlignment="Center" VerticalAlignment="Bottom"
Text="{Binding CurrentExtentString}"/>
  • 运行程序,发现范围描述并没有按预期显示出来。

为使数据绑定按预期工作,必须在绑定的属性梗概是触发notification事件。当前情况下在更改视图范围属性前就已经将其绑定到界面文本。当属性值更新时从未通知界面获取更新值,所以界面文本不会改变。

如果是这样的话,为什么绑定的地图工作?ViewModel的incidentmap属性设置在类的构造函数中,设置页面的数据上下文之前就已经赋值。

为了实现属性值改变的通知,我们需要实现System.ComponentModel 命名空间下的 INotifyPropertyChanged 接口。

  • 在MapViewModel中添加以下引用。
1
2
using System.ComponentModel;
using System.Runtime.CompilerServices;
  • 在MapViewModel中实现INotifyPropertyChanged接口
1
2
3
class MapViewModel : INotifyPropertyChanged
{
...
  • 添加以下代码实现PropertyChangedEventHandler
1
2
3
4
5
6
7
8
public event PropertyChangedEventHandler PropertyChanged;
private void RaiseNotifyPropertyChanged([CallerMemberName]string propertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
  • 在currentextentstring属性的set访问器中调用RaiseNotifyPropertyChanged函数触发notification。
1
2
3
4
5
6
7
8
9
10
11
12
13
private string extentString;
public string CurrentExtentString
{
get
{
return this.extentString;
}
set
{
this.extentString = value;
this.RaiseNotifyPropertyChanged();
}
}
  • 运行程序。当前视图范围显示在地图底部。

至此我们已完成了整个教程。如果你按此教程自己动手实现了。那么你应该对如何创建MVVM模式的ArcgisRuntime应用有了自己的理解。包括:数据绑定、Command、事件触发、属性变更通知。为了体会MVVM模式的有效性。你可以新建一个项目(也可以是其他.net平台),在不同界面下重用本例的ViewModel和Command。

本教程源码:

ArcGisRuntime_MVVM_Demo

了解更多

要了解更多关于MVVM的知识请参阅以下链接:

原文地址:https://developers.arcgis.com/net/10-2/desktop/guide/use-the-mvvm-design-pattern.htm

最后更新: 2018年03月25日 15:16

原始链接: http://rickeryan.tk/2017/05/26/use-mvvm-in-ArcgisRuntime-app/

× 打赏作者~
打赏二维码