[Architecture] MVVM

[Application] : MVVM 模式

動機 :

開發應用程式的時候,針對使用者介面開發。
業界有許多前輩提出了多種的設計模式,其中最為人所知的就是 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。
後面的實作章節,將會使用這些物件,來完成「新增使用者」的功能。

 

using System;
using System.Data;
using System.Data.SqlClient;

namespace MvcSamples.Domain
{
    public class User
    {
        // Properties
        public string Id { get; set; }

        public string Name { get; set; }
    }

    public interface IUserRepository
    {
        // Methods
        void Add(User item);
    }
}

namespace MvcSamples.Domain.Concretion
{
    public class SqlUserRepository : IUserRepository
    {
        // Fields
        private readonly string _connectionString = @"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Concretion\SqlMvcSamplesDatabase.mdf;Integrated Security=True;User Instance=True";


        // Methods
        private SqlConnection CreateConnection()
        {
            return new SqlConnection(_connectionString);
        }

        public void Add(User item)
        {
            #region Require

            if (item == null) throw new ArgumentNullException();

            #endregion
            SqlCommand command;
            using (SqlConnection connection = this.CreateConnection())
            {
                // Connection
                connection.Open();

                // Insert User
                using (command = connection.CreateCommand())
                {
                    command.CommandType = CommandType.Text;
                    command.CommandText = "INSERT INTO [User](Id, Name) VALUES (@Id, @Name)";
                    command.Parameters.AddWithValue("@Id", item.Id);
                    command.Parameters.AddWithValue("@Name", item.Name);
                    command.ExecuteNonQuery();
                }
            }
        }
    }
}

 

實作 - 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裡面,包含了整個系統會使用到的資料物件、邊界物件、邏輯物件...等等。
4. 以DDD的觀念去分析ViewModel,可以將ViewModel視為Application Layer。
  這個Application Layer封裝View所需要的資料、操作及狀態維持,用來提供給View使用。

 

 

經過這些分析與設計的種種考量,可以設計出如下圖的物件圖。

 

 

*實作程式
有了物件圖,剩下的就只是建立物件的實作程式碼。
這邊選擇能簡易套用 MVVM的 WPF當做範例的介面框架,示範如何實作MVVM模式。

 

 

首先先建立一個ActionCommand物件,讓我們後續方便把函式包裝成Binding所支援的ICommand。

 

using System;
using System.Windows.Input;

namespace MvcSamples.Mvvm.Infrastructure
{
    public class ActionCommand : ICommand
    {
        // Fields
        private readonly Action _action = null;

        private bool _canExecute = true;


        // Constructor
        public ActionCommand(Action action)
            : this(action, true)
        {

        }

        public ActionCommand(Action action, bool canExecute)
        {
            #region Require

            if (action == null) throw new ArgumentNullException();

            #endregion
            _action = action;
            _canExecute = canExecute;
        }


        // Methods
        public void SetCanExecute(bool canExecute)
        {
            _canExecute = canExecute;
            this.OnCanExecuteChanged(this, EventArgs.Empty);
        }   

        public bool CanExecute(object parameter)
        {
            return _canExecute;
        }        

        public void Execute(object parameter)
        {
            if (this.CanExecute(parameter) == false)
            {
                throw new InvalidOperationException();
            }
            else
            {
                _action(); 
            }
        }


        // Events
        public event EventHandler CanExecuteChanged;
        private void OnCanExecuteChanged(object sender, EventArgs e)
        {
            #region Require

            if (sender == null) throw new ArgumentNullException();
            if (e==null) throw new ArgumentNullException();

            #endregion
            EventHandler eventHandler = this.CanExecuteChanged;
            if (eventHandler != null)
            {
                eventHandler(sender, e);
            }
        }
    }
}

 

再來建立UserViewModel物件,封裝提供給View使用的資料與操作。
並且加上UserViewModelRepository物件、IUserViewModelRepositoryProvider介面,做為UserViewModel進出邊界的介面。

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MvcSamples.Mvvm.ViewModel
{
    public interface IUserViewModelRepositoryProvider
    {
        // Methods
        void Add(UserViewModel item);
    }
}

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MvcSamples.Mvvm.ViewModel
{   
    public class UserViewModelRepository 
    {
        // Fields
        private readonly IUserViewModelRepositoryProvider _provider = null;


        // Constructor
        public UserViewModelRepository(IUserViewModelRepositoryProvider provider)
        {
            #region Require

            if (provider == null) throw new ArgumentNullException();

            #endregion
            _provider = provider;
        }


        // Methods
        public void Add(UserViewModel item)
        {
            #region Require

            if (item == null) throw new ArgumentNullException();

            #endregion
            _provider.Add(item);
        }
    }
}

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace MvcSamples.Mvvm.ViewModel
{    
    public class UserViewModel : INotifyPropertyChanged
    {
        // Fields
        private string _id = null;

        private string _name = null;


        // Constructor
        public UserViewModel()
        {
            _id = string.Empty;
            _name = string.Empty;
        }
        

        // Properties
        public string Id 
        {
            get 
            {
                return _id;
            }
            set
            {
                _id = value;
                this.OnPropertyChanged("Id");
            } 
        }

        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
                this.OnPropertyChanged("Name");
            }
        }
              

        // Events
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            #region Require

            if (string.IsNullOrEmpty(propertyName) == true) throw new ArgumentNullException();

            #endregion
            PropertyChangedEventHandler propertyChangedEventHandler = this.PropertyChanged;
            if (propertyChangedEventHandler != null)
            {
                propertyChangedEventHandler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

 

接著就是建立AddUserViewModel物件,封裝提供給View使用的資料與操作。

 

using System;
using System.ComponentModel;
using System.Windows.Input;
using MvcSamples.Mvvm.Infrastructure;
using MvcSamples.Mvvm.ViewModel;

namespace MvcSamples.Mvvm.ViewModel
{    
    public class AddUserViewModel : INotifyPropertyChanged
    {
        // Fields
        private readonly UserViewModelRepository _userViewModelRepository = null;

        private readonly ICommand _addUserCommand = null;

        private UserViewModel _userViewModel = null;      


        // Constructor
        public AddUserViewModel(UserViewModelRepository userViewModelRepository)
        {
            #region Require

            if (userViewModelRepository == null) throw new ArgumentNullException();

            #endregion
            _userViewModelRepository = userViewModelRepository;
            _addUserCommand = new ActionCommand(this.AddUser);  
            _userViewModel = new UserViewModel();                     
        }


        // Properties
        public UserViewModel User
        {
            get
            {
                return _userViewModel;
            }
            private set
            {
                _userViewModel = value;
                this.OnPropertyChanged("User");
            }
        }
        
        public ICommand AddUserCommand
        {
            get
            {
                return _addUserCommand;
            }
        }


        // Methods
        private void AddUser()
        {
            _userViewModelRepository.Add(this.User);
            this.User = new UserViewModel();
        }


        // Events
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            #region Require

            if (string.IsNullOrEmpty(propertyName) == true) throw new ArgumentNullException();

            #endregion
            PropertyChangedEventHandler propertyChangedEventHandler = this.PropertyChanged;
            if (propertyChangedEventHandler != null)
            {
                propertyChangedEventHandler(this, new PropertyChangedEventArgs(propertyName));
            }
        }        
    }
}

 

繼續建立UserViewModelRepositoryProvider,用來讓整個模式跟Domain連接。

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MvcSamples.Mvvm.ViewModel;

namespace MvcSamples.Mvvm.ViewModel.Concretion
{
    public class UserViewModelRepositoryProvider : IUserViewModelRepositoryProvider
    {
        // Fields
        private readonly MvcSamples.Domain.IUserRepository _userRepository = null;


        // Constructor
        public UserViewModelRepositoryProvider(MvcSamples.Domain.IUserRepository userRepository)
        {
            #region Require

            if (userRepository == null) throw new ArgumentNullException();

            #endregion
            _userRepository = userRepository;
        }


        // Methods
        private MvcSamples.Domain.User CreateUser(UserViewModel item)
        {
            #region Require

            if (item == null) throw new ArgumentNullException();

            #endregion
            MvcSamples.Domain.User user = new MvcSamples.Domain.User();
            user.Id = item.Id;
            user.Name = item.Name;
            return user;
        }


        public void Add(UserViewModel item)
        {
            #region Require

            if (item == null) throw new ArgumentNullException();

            #endregion
            _userRepository.Add(this.CreateUser(item));
        }
    }
}

 

建立完上述的程式碼之後,額外再加一個AddUserViewModelHost。
用來提供無參數的建構物件,方便後續作Binding的操作。

 

using MvcSamples.Domain;
using MvcSamples.Domain.Concretion;
using MvcSamples.Mvvm.ViewModel;
using MvcSamples.Mvvm.ViewModel.Concretion;
using MvcSamples.Mvvm.ViewModel;

namespace MvcSamples.Mvvm.Runtime
{
    public class AddUserViewModelHost
    {
        // Fields
        private AddUserViewModel _viewModel = null;


        // Properties
        public AddUserViewModel ViewModel
        {
            get
            {
                if (_viewModel == null)
                {
                    _viewModel = this.Create();
                }
                return _viewModel;
            }
        }


        // Methods
        private AddUserViewModel Create()
        {
            IUserRepository userRepository = new SqlUserRepository();

            IUserViewModelRepositoryProvider userViewModelRepositoryProvider = new UserViewModelRepositoryProvider(userRepository);

            UserViewModelRepository userViewModelRepository = new UserViewModelRepository(userViewModelRepositoryProvider);

            return new AddUserViewModel(userViewModelRepository);
        }
    }
}

 

最後就是建立顯示用的XAML。

 

<Window x:Class="MvcSamples.Mvvm.WpfDemoApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:viewModel="clr-namespace:MvcSamples.Mvvm.ViewModel;assembly=MvcSamples.Mvvm"   
        xmlns:runtime="clr-namespace:MvcSamples.Mvvm.Runtime;assembly=MvcSamples.Mvvm"   
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <runtime:AddUserViewModelHost x:Key="addUserViewModelHost" />        
    </Window.Resources>
    <Window.DataContext>
        <Binding Source="{StaticResource addUserViewModelHost}" Path="ViewModel" Mode="OneTime" />
    </Window.DataContext>
    <Grid>
        <TextBox Name="textBox1" Height="23" Margin="10,10,0,0"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="120" DataContext="{Binding User}"  Text="{Binding Id}" />
        <TextBox Name="textBox2" Height="23" Margin="10,39,0,0"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="120" DataContext="{Binding User}"  Text="{Binding Name}" />
        <Button  Name="button1"  Height="23" Margin="55,68,0,0"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="75"  Content="Button" Command="{Binding AddUserCommand}" />
    </Grid>
</Window>

 

結果 :

 

編譯後執行, 在畫面上輸入資料並按下按鈕。於程式的中斷點做檢查,可以發現程式有正常執行。



期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。