Unity應用架構設計(1)—— MVVM 模式的設計和實施(Part 1)

初識 MVVM

談起 MVVM 設計模式,可能第一映像你會想到 WPF/Sliverlight,他們提供了的數據綁定(Data Binding),命令(Command)等功能,這讓 MVVM 模式獲得很好的實現。MVVM 設計模式顧名思義,經過分離關注點,各司其職。經過 Data Binding 可達到數據的雙向綁定,而命令 Command 更是將傳統的 Code Behind 事件獨立到 ViewModel 中。
git

MVVM 設計模式在 WPF 中的實現

在WPF中,你會像以下這樣去定義一個專門管理視圖 View 的 ViewModel:github

public class SongViewModel : INotifyPropertyChanged
{
    #region Construction
    /// Constructs the default instance of a SongViewModel
    public SongViewModel() {
        _song = new Song { ArtistName = "Unknown", SongTitle = "Unknown" };
    }
    #endregion

    #region Members
    Song _song;
    #endregion

    #region Properties
    public Song Song
    {
        get
        {
            return _song;
        }
        set
        {
            _song = value;
        }
    }

    public string ArtistName
    {
        get { return Song.ArtistName; }
        set
        {
            if (Song.ArtistName != value)
            {
                Song.ArtistName = value;
                RaisePropertyChanged("ArtistName");
            }
        }
    }
    #endregion

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

    #region Methods

    private void RaisePropertyChanged(string propertyName) {
        // take a copy to prevent thread issues
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    #endregion
}複製代碼

同時在 View 中你須要使用 Binding 將 ViewModel 的屬性綁定和控件的內容相綁定:設計模式

<TextBox Content="{Binding ArtistName}" />複製代碼

值得注意的是,要實現 View 和 ViewModel 雙向綁定,咱們的 ViewModel 必須實現 INotifyPropertyChanged 接口,因爲 WPF Framework 讓控件監聽了 PropertyChanged 事件,當屬性值發生時,觸發 PropertyChanged 事件,因此控件就能自動獲取到最新的值。反之,當控件的值發生改變時,例如 TextBox 觸發 OnTextChanged 事件,自動將最新的值同步到 ViewModel 相應的屬性中。框架

MVP & MVVM

Unity與 WPF/Sliverlight 不一樣,它沒有提供相似的 Data Binding,也沒有像 XAML 同樣的視圖語法,那麼怎樣才能在 Unity 3D 中去實現 MVVM 呢?mvvm

在 ASP.NET WebForm 時代,那時尚未 ASP.Net MVC 。咱們爲了讓 UI 表現層分離,經常會使用 MVP 設計模式,如下是我在幾年前畫的一張老圖:ide

MVP 設計模式核心就是,經過定義一個 View,將 UI 抽象出來,它沒必要關心數據的具體來源,也沒必要關心點擊按鈕以後業務邏輯的實現,它只關注 UI 交互。這就是典型的分離關注點。函數

其實這就是我今天想講的主題,既然 Unity 3D 沒有提供數據綁定,那麼咱們也能夠參考以前 MVP 的設計理念:ui

將 UI 抽象成獨立的一個個 View,將面向 Component 開發轉換爲面向 View 開發,每個 View 都有獨立的 ViewModel 進行管理,以下所示:this

因爲 Unity沒有 XAML,也沒有 Data Binding 技術,故只能在抽象出來的 View 中去實現相似於 WPF 的 Data Binding,Converter,Command 等。spa

值得注意的是,MVP 設計模式中數據的綁定是經過將具體的 View 實例傳遞到 Presenter 中完成的,而 MVVM 是以數據改變引起的事件中完成數據更新的。

MVVM 設計模式在 Unity 中的設計與實現

再回顧一下 WPF 中 ViewModel 的寫法。 ViewModel 提供了 View 須要的數據,而且 ViewModel 實現 INotifyPropertyChanged 接口 ,當數據更改時,觸發了 PropertyChanged 事件,因爲控件也監聽了此事件,在事件的響應函數裏實現數據的更新。

瞭解了以後,咱們要考慮怎樣在 Unity中去實現它。假設咱們須要完成以下的一個功能,而且是使用 MVVM 設計思想實現:

首先,咱們要定義一個 View,這個 View 是對 UI 元素的一個抽象,到底要抽象哪些 UI 元素呢?就這個例子而言,InputField,Label,Slider,Toggle,Button 是須要被抽象出來的。

public class SetupView
{
    public InputField nameInputField;
    public Text nameMessageText;
    public InputField jobInputField;
    public Text jobMessageText;

    public InputField atkInputField;
    public Text atkMessageText;

    public Slider successRateSlider;
    public Text successRateMessageText;

    public Toggle joinToggle;
    public Button joinInButton;
    public Button waitButton;
}複製代碼

能夠看到,這是一個很簡單的 View。接着咱們須要定義一個專門用來管理 View 的 ViewModel,它以屬性的形式提供數據,以方法的形式提供行爲。

值得注意的是,ViewModel 中的屬性不是特殊的屬性,它必須具有當數據更改時通知訂閱者這個功能,怎麼通知訂閱者?固然是事件,故我把此屬性稱爲 BindableProperty 屬性。

public class BindableProperty<T> {
    public delegate void ValueChangedHandler(T oldValue, T newValue);

    public ValueChangedHandler OnValueChanged;

    private T _value;
    public T Value
    {
        get
        {
            return _value;
        }
        set
        {
            if (!object.Equals(_value, value))
            {
                T old = _value;
                _value = value;
                ValueChanged(old, _value);
            }
        }
    }

    private void ValueChanged(T oldValue, T newValue) {
        if (OnValueChanged != null)
        {
            OnValueChanged(oldValue, newValue);
        }
    }

    public override string ToString() {
        return (Value != null ? Value.ToString() : "null");
    }
}複製代碼

接着,咱們再定義一個 ViewModel,它爲 View 提供了數據和行爲:

public class SetupViewModel : ViewModel
{
    public BindableProperty<string> Name = new BindableProperty<string>();
    public BindableProperty<string> Job = new BindableProperty<string>();
    public BindableProperty<int> ATK = new BindableProperty<int>();
    public BindableProperty<float> SuccessRate = new BindableProperty<float>();
    public BindableProperty<State> State = new BindableProperty<State>();
}複製代碼

有了 View 與 ViewModel 以後,咱們須要考慮:

  • 怎樣爲 View 指定一個 ViewModel
  • 當 ViewModel 屬性值改變時,怎樣訂閱觸發的 OnValueChanged 事件,從而達到 View 的數據更新

基於以上兩點,咱們能夠定義一個通用的 View,將它命名爲 UnityGuiView

public interface IView
{
    ViewModel BindingContext { get; set; }
}複製代碼
public class UnityGuiView:MonoBehaviour,IView
{
    public readonly BindableProperty<ViewModel> ViewModelProperty = new BindableProperty<ViewModel>();
    public ViewModel BindingContext
    {
        get { return ViewModelProperty.Value; }
        set { ViewModelProperty.Value = value; }
    }

    protected virtual void OnBindingContextChanged(ViewModel oldViewModel, ViewModel newViewModel)
    {
    }

    public UnityGuiView()
    {
        this.ViewModelProperty.OnValueChanged += OnBindingContextChanged;
    }

}複製代碼
  • 上述代碼中,提供一個 BindingContext 上下文屬性,相似於 WPF 中的 DataContext。 BindingContext 屬性咱們不能將它視爲一個簡單的屬性 ,它是上述定義過的 BindableProperty 類型屬性。那麼當爲一個 View 的 BindingContext 指定 ViewModel 實例時,初始化時,勢必會觸發 OnValueChanged 事件。
  • 在響應函數 OnBindingContextChanged 中 ,咱們能夠在此對 ViewModel 中事件進行監聽,從而達到數據的更新。固然這是一個虛方法,你須要在子類 View 中 Override。

因此修改定義過的 SetupView,繼承自 UnityGuiView:

public class SetupView:UnityGuiView
{
   ...省略部分代碼

   public SetupViewModel ViewModel { get { return (SetupViewModel)BindingContext; } }

   protected override void OnBindingContextChanged(ViewModel oldViewModel, ViewModel newViewModel) {

        base.OnBindingContextChanged(oldViewModel, newViewModel);

        SetupViewModel oldVm = oldViewModel as SetupViewModel;
        if (oldVm != null)
        {
            oldVm.Name.OnValueChanged -= NameValueChanged;
            ...
        }
        if (ViewModel!=null)
        {
            ViewModel.Name.OnValueChanged += NameValueChanged;
            ...
        }
        UpdateControls();
    }

    private void NameValueChanged(string oldvalue, string newvalue) {
        nameMessageText.text = newvalue.ToString();
    }
}複製代碼

因爲子類 Override 了 OnBindingContextChanged 方法,故它會對 ViewModel 的屬性值改變事件進行監聽,當觸發時,將最新的數據同步到 UI 中。

同理,考慮到雙向綁定,你也能夠在 View 中定義一個 OnTextBoxValueChanged 響應函數,當文本框中的數據改變時,在響應函數中就數據同步到 ViewModel 中。在這我就不累述了。

最後,在 Unity 3D 中將 SetupView 附加到 相應的 GameObject上:

最後在攝像機上加一段腳本,很簡單,傳入 SetupView 對象併爲其綁定 ViewModel:

public SetupView setupView;
void Start() {
    //綁定上下文
    setupView.BindingContext=new SetupViewModel();
}複製代碼

小結

這是一個很是簡單的 MVVM 框架,也證實了在 Unity中實現 MVVM 設計模式的可能性。
源代碼託管在Github上,點擊此瞭解

歡迎關注個人公衆號:

相關文章
相關標籤/搜索