转自http://www.dotblogs.com.tw/clark/archive/2011/10/02/38567.aspx
动机:
开发应用程式的时候,针对使用者介面开发。
业界有许多前辈提出了多种的设计模式,其中最为人所知的就是MVC模式。
MVC模式在实作上有许多种的方法,
不同的开发人员去理解它,都会有不同的理解。
不同的情景需求去套用它,也会有不同的实作。
但不论怎么理解跟实作,它最基本的观念依然都是:
「将系统职责拆解至Model、View、XXX三种类别,并且定义它们之间的相依关系及沟通方式。」
在微软.NET技术架构下,目前最为众人讨论的MVC延伸模式,
应该是适用WPF、Silverlight、Windows phone平台的MVVM模式(Model-View-ViewModel)。
可以说近年微软.NET架构下新推出的介面框架,多是主打套用这个设计模式。
本篇文章使用领域驱动设计的方式去分析设计,并且实作使用Domain Object的MVVM模式。
希望能透过这样的方式,让开发人员能对模式概念及如何实作有进一步的了解。
*这边要强调,本文的设计模式都是概念式模式。 每个人都有不同的理解跟实作,没有谁是绝对正确的跟错误的。
相关资料可以参考:
定义:
在开始设计模式的实作之前,还需要为后续的实作加上一些定义。
*执行状态首先来讨论「执行状态」这个定义。
以HTML为基础的Web网页,属于无状态的应用程式模型。
而相对于它的WinForm应用程式,就属于有状态的应用程式模型。
投射到物件上,也是有相同的概念。
可以依照物件在系统执行生命周期里,它的执行状态是否留存在系统内,
来区分为有状态的物件模型及无状态物件模型。
「执行状态」这个定义,会影响到实作设计模式的难易度。
当我们在一个无状态的应用程式模型上,选择实作某个有状态的物件模型。
在这种情景下,执行状态的维持就需要开发人员,在系统内作额外的设计。
*物件生成再来讨论「物件生成」这个定义。
当一个模式里有多个物件在交互运作的时候,哪个物件从哪边取得,是一件很重要的职责。
这里所谓的取得,不单单是指所谓的建立(Creation),也包含了注入(Inversion)等动作。
「物件生成」这个定义,会影响到物件相依性、建立物件的顺序及来源。
大多的设计模式都隐含了这个定义,但大多也都没有特别描述这个定义。
因为这有太多的实作方式,各种不同的组合会带来不同的效益。
但仔细参考设计模式文件的范例程式,可以去理解到各个设计模式隐含的物件生成职责。
范例:
本篇文章物件模型拆解的比较琐碎,建议开发人员下载范例程式后。
开启专案做对照,能比较容易理解文字描述的内容。
范例原始码 : 点此下载
实作- Domain :
本文实作一个「新增使用者」的功能,来当作设计模式的范例。
这个功能情景很简单,
1. 使用者输入使用者资料。
2. 使用者资料存入SQL资料库。
3. 清空使用者资料等待输入。
而使用者资料的栏位,单纯的只有编号跟姓名两个栏位。
依照这个功能描述,使用领域驱动设计的方式去分析设计。
我们可以先得到一个领域物件User。
以及一个将User资料进出系统的边界介面IUserRepository。
还有一个实际将User资料存入SQL资料库的资料存取物件SqlUserRepository。
后面的实作章节,将会使用这些物件,来完成「新增使用者」的功能。
01
using System;
02
using System.Data;
03
using System.Data.SqlClient;
04
05
namespace MvcSamples.Domain
06 {
07
public
class User
08 {
09
//
Properties
10
public
string Id {
get;
set; }
11
12
public
string Name {
get;
set; }
13 }
14
15
public
interface IUserRepository
16 {
17
//
Methods
18
void Add(User item);
19 }
20 }
21
22
namespace MvcSamples.Domain.Concretion
23 {
24
public
class SqlUserRepository : IUserRepository
25 {
26
//
Fields
27
private
readonly
string _connectionString =
@"
Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Concretion\SqlMvcSamplesDatabase.mdf;Integrated Security=True;User Instance=True
";
28
29
30
//
Methods
31
private SqlConnection CreateConnection()
32 {
33
return
new SqlConnection(_connectionString);
34 }
35
36
public
void Add(User item)
37 {
38
#region Require
39
40
if (item ==
null)
throw
new ArgumentNullException();
41
42
#endregion
43 SqlCommand command;
44
using (SqlConnection connection =
this.CreateConnection())
45 {
46
//
Connection
47 connection.Open();
48
49
//
Insert User
50
using (command = connection.CreateCommand())
51 {
52 command.CommandType = CommandType.Text;
53 command.CommandText =
"
INSERT INTO [User](Id, Name) VALUES (@Id, @Name)
";
54 command.Parameters.AddWithValue(
"
@Id
", item.Id);
55 command.Parameters.AddWithValue(
"
@Name
", item.Name);
56 command.ExecuteNonQuery();
57 }
58 }
59 }
60 }
61 }
实作- MVVM模式:
*模式结构下图是MVVM模式的结构图,很简单的就是将系统拆解成三个类别(Model、View、ViewModel)。
各个类别的主要职责为:Model负责企业资料逻辑、View负责画面资料逻辑、ViewModel负责执行状态维持、画面流程逻辑及企业流程逻辑。
其中ViewModel-Model之间,是ViewModel直接使用Model开放的成员,属于ViewModel到Model的单向沟通连接。
而View-ViewModel之间,是透过Binding技术及Command的设计模式,将两者作双向的沟通连接。
*模式特征做为MVC延伸模式的MVVM模式,其最大的特征就是,
在View-ViewModel之间,是透过Binding技术及Command的设计模式,将两者作双向的沟通连接。
并且在模型结构设计上,将ViewModel定义为有状态的物件模型,由ViewModel负责维持执行状态。
这样设计最大的好处,是可以将View与ViewModel之间的相依关系,设计为单向相依。
ViewModel做是独立的个体不相依View,让View的职责回归到单纯的完成输入及显示的工作。
并且方便特定的设计工具设计View的外观,可以将View的设计交由完全不懂程式设计的人员作处理。
*实作分析
1. MVVM模式本身在模型结构设计上,是将ViewModel设计为有状态的物件模型。
实作范例的内容,将ViewModel架构在有状态的应用程式模型上,不做额外的设计。
2. 而MVVM模式物件之间的生成模式,实作上设计成以View当作主要物件,生成ViewModel及Model,并且将Model注入至ViewModel。
3. 以DDD的观念去分析Model,可以将Model视为Domain Layer,是整个模式重用的焦点。
这个Domain Layer里面,包含了整个Presentation会使用到的资料物件、边界物件、逻辑物件...等等。
4. 以DDD的观念去分析ViewModel,可以将ViewModel视为Application Layer。
这个Application Layer封装View所需要的资料、操作及状态维持,用来提供给View使用。
经过这些分析与设计的种种考量,可以设计出如下图的物件图。
*实作程式有了物件图,剩下的就只是建立物件的实作程式码。
这边选择能简易套用MVVM的WPF当做范例的介面框架,示范如何实作MVVM模式。
首先先建立一个ActionCommand物件,让我们后续方便把函式包装成Binding所支援的ICommand。
01
using System;
02
using System.Windows.Input;
03
04
namespace MvcSamples.Mvvm.Infrastructure
05 {
06
public
class ActionCommand : ICommand
07 {
08
//
Fields
09
private
readonly Action _action =
null;
10
11
private
bool _canExecute =
true;
12
13
14
//
Constructor
15
public ActionCommand(Action action)
16 :
this(action,
true)
17 {
18
19 }
20
21
public ActionCommand(Action action,
bool canExecute)
22 {
23
#region Require
24
25
if (action ==
null)
throw
new ArgumentNullException();
26
27
#endregion
28 _action = action;
29 _canExecute = canExecute;
30 }
31
32
33
//
Methods
34
public
void SetCanExecute(
bool canExecute)
35 {
36 _canExecute = canExecute;
37
this.OnCanExecuteChanged(
this, EventArgs.Empty);
38 }
39
40
public
bool CanExecute(
object parameter)
41 {
42
return _canExecute;
43 }
44
45
public
void Execute(
object parameter)
46 {
47
if (
this.CanExecute(parameter) ==
false)
48 {
49
throw
new InvalidOperationException();
50 }
51
else
52 {
53 _action();
54 }
55 }
56
57
58
//
Events
59
public
event EventHandler CanExecuteChanged;
60
private
void OnCanExecuteChanged(
object sender, EventArgs e)
61 {
62
#region Require
63
64
if (sender ==
null)
throw
new ArgumentNullException();
65
if (e==
null)
throw
new ArgumentNullException();
66
67
#endregion
68 EventHandler eventHandler =
this.CanExecuteChanged;
69
if (eventHandler !=
null)
70 {
71 eventHandler(sender, e);
72 }
73 }
74 }
75 }
再来建立UserViewModel物件,封装提供给View使用的资料与操作。
并且加上UserViewModelRepository物件、IUserViewModelRepositoryProvider介面,做为UserViewModel进出边界的介面。
01
using System;
02
using System.Collections.Generic;
03
using System.Linq;
04
using System.Text;
05
06
namespace MvcSamples.Mvvm.ViewModel
07 {
08
public
interface IUserViewModelRepositoryProvider
09 {
10
//
Methods
11
void Add(UserViewModel item);
12 }
13 }
01
using System;
02
using System.Collections.Generic;
03
using System.Linq;
04
using System.Text;
05
06
namespace MvcSamples.Mvvm.ViewModel
07 {
08
public
class UserViewModelRepository
09 {
10
//
Fields
11
private
readonly IUserViewModelRepositoryProvider _provider =
null;
12
13
14
//
Constructor
15
public UserViewModelRepository(IUserViewModelRepositoryProvider provider)
16 {
17
#region Require
18
19
if (provider ==
null)
throw
new ArgumentNullException();
20
21
#endregion
22 _provider = provider;
23 }
24
25
26
//
Methods
27
public
void Add(UserViewModel item)
28 {
29
#region Require
30
31
if (item ==
null)
throw
new ArgumentNullException();
32
33
#endregion
34 _provider.Add(item);
35 }
36 }
37 }
01
using System;
02
using System.Collections.Generic;
03
using System.Linq;
04
using System.Text;
05
using System.ComponentModel;
06
07
namespace MvcSamples.Mvvm.ViewModel
08 {
09
public
class UserViewModel : INotifyPropertyChanged
10 {
11
//
Fields
12
private
string _id =
null;
13
14
private
string _name =
null;
15
16
17
//
Constructor
18
public UserViewModel()
19 {
20 _id =
string.Empty;
21 _name =
string.Empty;
22 }
23
24
25
//
Properties
26
public
string Id
27 {
28
get
29 {
30
return _id;
31 }
32
set
33 {
34 _id = value;
35
this.OnPropertyChanged(
"
Id
");
36 }
37 }
38
39
public
string Name
40 {
41
get
42 {
43
return _name;
44 }
45
set
46 {
47 _name = value;
48
this.OnPropertyChanged(
"
Name
");
49 }
50 }
51
52
53
//
Events
54
public
event PropertyChangedEventHandler PropertyChanged;
55
private
void OnPropertyChanged(
string propertyName)
56 {
57
#region Require
58
59
if (
string.IsNullOrEmpty(propertyName) ==
true)
throw
new ArgumentNullException();
60
61
#endregion
62 PropertyChangedEventHandler propertyChangedEventHandler =
this.PropertyChanged;
63
if (propertyChangedEventHandler !=
null)
64 {
65 propertyChangedEventHandler(
this,
new PropertyChangedEventArgs(propertyName));
66 }
67 }
68 }
69 }
接着就是建立AddUserViewModel物件,封装提供给View使用的资料与操作。
01
using System;
02
using System.ComponentModel;
03
using System.Windows.Input;
04
using MvcSamples.Mvvm.Infrastructure;
05
using MvcSamples.Mvvm.ViewModel;
06
07
namespace MvcSamples.Mvvm.ViewModel
08 {
09
public
class AddUserViewModel : INotifyPropertyChanged
10 {
11
//
Fields
12
private
readonly UserViewModelRepository _userViewModelRepository =
null;
13
14
private
readonly ICommand _addUserCommand =
null;
15
16
private UserViewModel _userViewModel =
null;
17
18
19
//
Constructor
20
public AddUserViewModel(UserViewModelRepository userViewModelRepository)
21 {
22
#region Require
23
24
if (userViewModelRepository ==
null)
throw
new ArgumentNullException();
25
26
#endregion
27 _userViewModelRepository = userViewModelRepository;
28 _addUserCommand =
new ActionCommand(
this.AddUser);
29 _userViewModel =
new UserViewModel();
30 }
31
32
33
//
Properties
34
public UserViewModel User
35 {
36
get
37 {
38
return _userViewModel;
39 }
40
private
set
41 {
42 _userViewModel = value;
43
this.OnPropertyChanged(
"
User
");
44 }
45 }
46
47
public ICommand AddUserCommand
48 {
49
get
50 {
51
return _addUserCommand;
52 }
53 }
54
55
56
//
Methods
57
private
void AddUser()
58 {
59 _userViewModelRepository.Add(
this.User);
60
this.User =
new UserViewModel();
61 }
62
63
64
//
Events
65
public
event PropertyChangedEventHandler PropertyChanged;
66
private
void OnPropertyChanged(
string propertyName)
67 {
68
#region Require
69
70
if (
string.IsNullOrEmpty(propertyName) ==
true)
throw
new ArgumentNullException();
71
72
#endregion
73 PropertyChangedEventHandler propertyChangedEventHandler =
this.PropertyChanged;
74
if (propertyChangedEventHandler !=
null)
75 {
76 propertyChangedEventHandler(
this,
new PropertyChangedEventArgs(propertyName));
77 }
78 }
79 }
80 }
继续建立UserViewModelRepositoryProvider,用来让整个模式跟Domain连接。
|
01
using System;
02
using System.Collections.Generic;
03
using System.Linq;
04
using System.Text;
05
using MvcSamples.Mvvm.ViewModel;
06
07
namespace MvcSamples.Mvvm.ViewModel.Concretion
08 {
09
public
class UserViewModelRepositoryProvider : IUserViewModelRepositoryProvider
10 {
11
//
Fields
12
private
readonly MvcSamples.Domain.IUserRepository _userRepository =
null;
13
14
15
//
Constructor
16
public UserViewModelRepositoryProvider(MvcSamples.Domain.IUserRepository userRepository)
17 {
18
#region Require
19
20
if (userRepository ==
null)
throw
new ArgumentNullException();
21
22
#endregion
23 _userRepository = userRepository;
24 }
25
26
27
//
Methods
28
private MvcSamples.Domain.User CreateUser(UserViewModel item)
29 {
30
#region Require
31
32
if (item ==
null)
throw
new ArgumentNullException();
33
34
#endregion
35 MvcSamples.Domain.User user =
new MvcSamples.Domain.User();
36 user.Id = item.Id;
37 user.Name = item.Name;
38
return user;
39 }
40
41
42
public
void Add(UserViewModel item)
43 {
44
#region Require
45
46
if (item ==
null)
throw
new ArgumentNullException();
47
48
#endregion
49 _userRepository.Add(
this.CreateUser(item));
50 }
51 }
52 }
|
建立完上述的程式码之后,额外再加一个AddUserViewModelHost。
用来提供无参数的建构物件,方便后续作Binding的操作。
|
01
using MvcSamples.Domain;
02
using MvcSamples.Domain.Concretion;
03
using MvcSamples.Mvvm.ViewModel;
04
using MvcSamples.Mvvm.ViewModel.Concretion;
05
using MvcSamples.Mvvm.ViewModel;
06
07
namespace MvcSamples.Mvvm.Runtime
08 {
09
public
class AddUserViewModelHost
10 {
11
//
Fields
12
private AddUserViewModel _viewModel =
null;
13
14
15
//
Properties
16
public AddUserViewModel ViewModel
17 {
18
get
19 {
20
if (_viewModel ==
null)
21 {
22 _viewModel =
this.Create();
23 }
24
return _viewModel;
25 }
26 }
27
28
29
//
Methods
30
private AddUserViewModel Create()
31 {
32 IUserRepository userRepository =
new SqlUserRepository();
33
34 IUserViewModelRepositoryProvider userViewModelRepositoryProvider =
new UserViewModelRepositoryProvider(userRepository);
35
36 UserViewModelRepository userViewModelRepository =
new UserViewModelRepository(userViewModelRepositoryProvider);
37
38
return
new AddUserViewModel(userViewModelRepository);
39 }
40 }
41 }
|
最后就是建立显示用的XAML。
01
<
Window
x:Class
="MvcSamples.Mvvm.WpfDemoApp.MainWindow"
02 xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
03 xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
04 xmlns:viewModel
="clr-namespace:MvcSamples.Mvvm.ViewModel;assembly=MvcSamples.Mvvm"
05 xmlns:runtime
="clr-namespace:MvcSamples.Mvvm.Runtime;assembly=MvcSamples.Mvvm"
06 Title
="MainWindow"
Height
="350"
Width
="525"
>
07
<
Window.Resources
>
08
<
runtime:AddUserViewModelHost
x:Key
="addUserViewModelHost"
/>
09
</
Window.Resources
>
10
<
Window.DataContext
>
11
<
Binding
Source
="
{StaticResource addUserViewModelHost}
"
Path
="ViewModel"
Mode
="OneTime"
/>
12
</
Window.DataContext
>
13
<
Grid
>
14
<
TextBox
Name
="textBox1"
Height
="23"
Margin
="10,10,0,0"
VerticalAlignment
="Top"
HorizontalAlignment
="Left"
Width
="120"
DataContext
="
{Binding User}
"
Text
="
{Binding Id}
"
/>
15
<
TextBox
Name
="textBox2"
Height
="23"
Margin
="10,39,0,0"
VerticalAlignment
="Top"
HorizontalAlignment
="Left"
Width
="120"
DataContext
="
{Binding User}
"
Text
="
{Binding Name}
"
/>
16
<
Button
Name
="button1"
Height
="23"
Margin
="55,68,0,0"
VerticalAlignment
="Top"
HorizontalAlignment
="Left"
Width
="75"
Content
="Button"
Command
="
{Binding AddUserCommand}
"
/>
17
</
Grid
>
18
</
Window
>
结果:
编译后执行, 在画面上输入资料并按下按钮。 于程式的中断点做检查,可以发现程式有正常执行。
期许自己~
能以更简洁的文字与程式码,传达出程式设计背后的精神。
真正做到「以形写神」的境界。