转自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; |
建立完上述的程式码之后,额外再加一个AddUserViewModelHost。
用来提供无参数的建构物件,方便后续作Binding的操作。
01 using MvcSamples.Domain; |
最后就是建立显示用的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>
结果:
编译后执行, 在画面上输入资料并按下按钮。 于程式的中断点做检查,可以发现程式有正常执行。
期许自己~
能以更简洁的文字与程式码,传达出程式设计背后的精神。
真正做到「以形写神」的境界。