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

木宛城主發表於2019-02-25

MVVM 回顧

經過上一篇文章的介紹,相信你對MVVM的設計思想有所瞭解。MVVM的核心思想就是解耦,View與ViewModel應該感受不到彼此的存在。View只關心怎樣渲染,而ViewModel只關心怎麼處理邏輯,整個架構由資料進行驅動。不僅View與ViewModel彼此解耦,ViewModel與ViewModel之間也是解耦的,通過訊息訂閱-釋出機制,解決了ViewModel之間的強依賴關係。

先回顧一下我們已完成的功能,框架中最核心就是BindableProperty類,ViewModel 中所有需要被繫結到UI 控制元件的屬性必須是一個BindableProperty物件。它是一個職責非常單一的類,監聽Value的數值是否發生變化,當變化時,觸發OnValueChanged 事件,通知View 做出相應的更新。

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);
        }
    }
}複製程式碼

那問題來了,View在何時並以怎樣的方式去監聽這些屬性的變化呢?

BindableProperty是一個很好的設計,它不僅可以用在ViewModel中,還可以用在View中,用它來修飾 ViewModel,當ViewModel 改變時,比如初始化時,或者從一個ViewModel變化到另一個ViewModel物件時,在觸發的OnBindingContextChanged 事件中實現對ViewModel中的屬性監聽。如下定義的抽象父類:UnityGuiView

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;
}複製程式碼

子類SetupView繼承自UnityGuiView,並且Override OnBindingContextChanged,並實現對ViewModel中的屬性監聽。

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;
        ...
    }
}複製程式碼

進一步抽象

實際上對於ViewModel而言會有非常多的BindableProperty需要被繫結到UI控制元件中,從程式碼的可讀性而言,如下程式碼是非常沉長和囉嗦的:

if (oldVm != null)
{
    oldVm.Name.OnValueChanged -= NameValueChanged;
    ...
}
if (ViewModel!=null)
{
    ViewModel.Name.OnValueChanged += NameValueChanged;
    ...
}複製程式碼

因為+=和-=是成對出現的,所以只要是看到 OnValueChanged,這部份程式碼的長度幾乎都是*2。

仔細觀察一下,每個View都會出現 具體的 ViewModel.屬性.OnValueChanged事件+=或者-=具體的處理函式 這樣的固定模板。
那麼是否可以將這部分程式碼抽象到一個公共類中呢,並且暴露出一個簡單的方法提供給View來初始化這些OnValueChanged事件,比如:

PropertyBindingUtils.Init<string>("Color",OnColorPropertyValueChanged);複製程式碼

然後在Init方法中+=或者-=具體的處理函式。

當然是可以得,定義一個PropertyBinder屬性繫結器,通過反射技術,動態為屬性+=或者-= OnValueChanged 事件,腦海裡的 Raw 程式碼如下

Init<TProperty>(string propertyName ,OnValueChanged valueChangedHandler)
{
    var fieldInfo = typeof(TViewModel).GetField(propertyName, BindingFlags.Instance | BindingFlags.Public);
    var value = fieldInfo.GetValue(viewModel);
    BindableProperty<TProperty> bindableProperty = value as BindableProperty<TProperty>;
    bindableProperty.OnValueChanged += valueChangedHandler;
    bindableProperty.OnValueChanged -= valueChangedHandler;
}複製程式碼

最核心的程式碼就那麼幾步,詳細程式碼可以檢視原始碼PropertyBinder的實現。

重構檢視基類:UnityGuiView

想象一下PropertyBinder應該放在哪兒?

  • 它是用來監聽ViewModel中的屬性值變化的,用來替換沉長的 oldVm.Property.OnValueChanged +=和-= NameValueChanged,理所應當應該放在View中
  • 因為每個View都需要,故將它定義在UnityGuiView 中。
  • 又因為PropertyBinder需要知道為哪個ViewModel進行服務(因為需要反射),故通過泛型來約束 UnityGuiView< T >:IView where T:ViewModelBase

再對BindingContext稍作改變,當它被賦值時,只初始化一次對OnValueChanged事件的監聽(原先是放在建構函式裡)。

public readonly BindableProperty<ViewModelBase> ViewModelProperty = new BindableProperty<ViewModelBase>();
public ViewModelBase BindingContext
{
    get { return ViewModelProperty.Value; }
    set
    {
        if (!_isBindingContextInitialized)
        {
            OnInitialize();
            _isBindingContextInitialized = true;
        }
        //觸發OnValueChanged事件
        ViewModelProperty.Value = value;
    }
}
/// <summary>
/// 初始化View,當BindingContext改變時執行
/// </summary>
protected virtual void OnInitialize()
{
    //無所ViewModel的Value怎樣變化,只對OnValueChanged事件監聽(繫結)一次
    ViewModelProperty.OnValueChanged += OnBindingContextChanged;
}複製程式碼

值得注意的事,我定義了一個virtual的OnInitialize,這樣子類可以override它從而實現一些初始化方法,比如:

    protected override void OnInitialize()
    {
        base.OnInitialize();
        Binder.Add<string>("Color",OnColorPropertyValueChanged);
    }

    private void OnColorPropertyValueChanged(string oldValue, string newValue)
    {
        switch (newValue)
        {
            case "Red":
                buttonImage.color = Color.red;
                break;
            case "Yellow":
                buttonImage.color = Color.yellow;
                break;
            default:
                break;
        }
    }複製程式碼

小節

這篇部落格基本上是回顧了MVVM模式在Unity 3D上的實踐,結合自己的開發經驗,通過反射的技術可以有效減少沉長的程式碼。
原始碼託管在Github上,點選此瞭解

歡迎關注我的公眾號:

相關文章