初識 MVVM
談起 MVVM 設計模式,可能第一映像你會想到 WPF/Sliverlight,他們提供了的資料繫結(Data Binding),命令(Command)等功能,這讓 MVVM 模式得到很好的實現。MVVM 設計模式顧名思義,通過分離關注點,各司其職。通過 Data Binding
可達到資料的雙向繫結,而命令 Command
更是將傳統的 Code Behind 事件獨立到 ViewModel 中。
MVVM 設計模式在 WPF 中的實現
在WPF中,你會像如下這樣去定義一個專門管理檢視 View 的 ViewModel:
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 呢?
在 ASP.NET WebForm 時代,那時還沒有 ASP.Net MVC 。我們為了讓 UI 表現層分離,常常會使用 MVP 設計模式,以下是我在幾年前畫的一張老圖:
MVP 設計模式核心就是,通過定義一個 View,將 UI 抽象出來,它不必關心資料的具體來源,也不必關心點選按鈕之後業務邏輯的實現,它只關注 UI 互動。這就是典型的分離關注點。
其實這就是我今天想講的主題,既然 Unity 3D 沒有提供資料繫結,那麼我們也可以參考之前 MVP 的設計理念:
將 UI 抽象成獨立的一個個 View,將面向 Component 開發轉換為面向 View 開發,每一個 View 都有獨立的 ViewModel 進行管理,如下所示:
由於 Unity沒有 XAML,也沒有 Data Binding 技術,故只能在抽象出來的 View 中去實現類似於 WPF 的 Data Binding,Converter,Command 等。
值得注意的是,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上,點選此瞭解
歡迎關注我的公眾號: