在Unity中,PureMVC适合用来做UI上的交互框架。市面上有相当一部分的游戏公司也是采用pureMVC来搭建UI框架。
根据自己初学这个框架的体会,新人学这个框架一定要先有个好的实例看看怎么运行怎么交互的,对这个框架理解才快,单纯看文档也是云里雾里的。 PureMVC在Unity的好的实例不是很容易找,所以这里通过一个简单实例以及官方文档PDF来阐明pureMVC的运行逻辑流程,框架的优点以及正确使用框架需要注意的事项,自己以后再回想也不需要重新查找和翻看资料,新学的同学可以打开项目然后边看博客边运行实例对照看看。
首先简单介绍一下PureMVC的概念,PureMVC里面有Mediator,Proxy,Command类,分别对应但是拓展了传统MVC模式的View,Model,Control。 Mediator,Proxy,的实例都是单例模式,在程序中只有一个,关于PureMVC的特点还有:
简单介绍完pureMVC的特点后,说下实例,实例的功能是显示一个菜单列表,在选中菜单列表某项之后,下面的UI会显示出详情,可以对选中的某项进行修改,也可以新加某一行,也可以删除某一行,也可以取消当前选中项的查看详情。
在实例中,用到的Command分别叫做StartUpCommand和DeleteCommand。StartUpCommand负责Mediator的初始化,DeleteCommand负责删除列表数据的逻辑操作;
用到的Proxy叫做UserProxy,它继承自Proxy类;
用到的Mediator分别是UserListMediator和UserFormMediator,持有的view分别叫做UserList和UserForm,下图中,顶部的UI叫做userList,底部的UI叫做UserForm;
UI结构如下
每一项的变绿是toggle选项实现的,toggle指定了toggleGroup,因为toggleGroup是没有勾选allowSwitchOff,所以数据加载完之后,一定会有一项变绿,即进入选中状态,从而触发pureMVC实现下部UI的详情显示。如果对toggleGroup不了解的人,第一次看代码对怎么选中可能有疑问,因为代码里面找不到最开始是哪里进行选择的,所以这里解释一下。
然后 相对于pureMVC的初始化事件注册来说,unity的列表加载完成要延迟那么几帧,所以即使一开始代码看起来好像明明先完成UserList列表加载的后面才进行UserFrom对选中某项的事件的初始化注册,UserForm还是能对选中的项进行详情显示
程序的开始点是在这里,实例化一个ApplicationFacade,调用其启动函数,将自己MainUI传进去
public class MainUI : MonoBehaviour
{
public UserList userList;
public UserForm userForm;
//Lucie Wilde
void Awake()
{
//Lucie Wilde
ApplicationFacade facade = ApplicationFacade.Instance as ApplicationFacade;
facade.Startup(this);
}
}
看看ApplicationFacade类
//[lzh]
using UnityEngine;
using System.Collections;
using PureMVC.Patterns;
using PureMVC.Interfaces;
public class ApplicationFacade : Facade
{
///
/// Facade Singleton Factory method. This method is thread safe.
///
public new static IFacade Instance
{
get
{
if(m_instance == null)
{
lock(m_staticSyncRoot)
{
if (m_instance == null)
{
Debug.Log("ApplicationFacade");
m_instance = new ApplicationFacade();
}
}
}
return m_instance;
}
}
///
/// Start the application
///
///
public void Startup(MainUI mainUI)
{
Debug.Log("Startup() to SendNotification.");
SendNotification(EventsEnum.STARTUP, mainUI);
}
protected ApplicationFacade()
{
// Protected constructor.
}
///
/// Explicit static constructor to tell C# compiler
/// not to mark type as beforefieldinit
///
static ApplicationFacade()
{
}
protected override void InitializeController()
{
Debug.Log("InitializeController()");
base.InitializeController();
RegisterProxy(new UserProxy());
RegisterCommand(EventsEnum.STARTUP, typeof(StartupCommand));
RegisterCommand(EventsEnum.DELETE_USER, typeof(DeleteUserCommand));
}
}
这个类用一个单例模式实现,它的写法可以参考
关于C#的Lock关键字实现线程安全的单例模式
调用单例模式会实例化一个ApplicationFacader对象,在pureMVC使用惯例中,程序都会有个叫做ApplicationFacader的对象,这个对象只有一个,所以用单例模式,这个对象继承自Facade类,继承自这个类的实例会持有程序中所有继承自Mediator的实例,继承自Proxy类的实例。要直接获取这些实例要通过ApplicationFacade,在创建ApplacationFacade的时候首先会执行InitializeController,按照PureMVC的惯例写法,这里一般先执行所有数据的注册和初始化,然后执行所有Command的注册和初始化,ApplacationFacade的InitializeController重写自Facade,Façade是与Mediator和Proxy这两个核心层通信的唯一接口,以简化开发复杂度。
继承自Facade的类要重写InitializeController这个方法, 案例在这个方法里面做了两件事,
RegisterProxy(new UserProxy());
RegisterCommand(EventsEnum.STARTUP, typeof(StartupCommand));
RegisterCommand(EventsEnum.DELETE_USER, typeof(DeleteUserCommand));
第一句是UserProxy类的实例化及注册
下面两句话对两个继承了Cammand类的类进行注册。
下面是StartUpCommand的内容
//[lzh]
using UnityEngine;
using System.Collections;
using PureMVC.Patterns;
using PureMVC.Interfaces;
public class StartupCommand : SimpleCommand, ICommand
{
public override void Execute(INotification notification)
{
Debug.Log("StartupCommand.Execute()");
MainUI mainUI = notification.Body as MainUI;
Facade.RegisterMediator(new UserListMediator(mainUI.userList));
Facade.RegisterMediator(new UserFormMediator(mainUI.userForm));
}
}
继承自Command的类只重写了Execute方法,继承自Command的类也只需重写Execute方法。
上面代码中的Execute方法并不是在下面这句话执行的时候执行的。
RegisterCommand(EventsEnum.STARTUP, typeof(StartupCommand));
注意Application类里面还有一句话
SendNotification(EventsEnum.STARTUP, mainUI);
上面两句话一个是注册StartupCommand,第二个就是对注册的StartupCommand进行唤起,并且唤起时传值了一个mainUI,即文章最开始的代码的MainUI类的实例,在调用SendNotification的时候传什么其实都可以,PureMVC的这个方法,后面可以传1个任意类型的参数,实际运用中传什么参数就是由开发者根据需要进行设计了,如果需要传多个参数的时候,可以将多个参数封装到一个对象里面传进来。
唤起的字符串标记常量,习惯全部写到某个类里面,如下,常量的名字已经代表了它唤起的时机和原因:
public class EventsEnum
{
public const string STARTUP = "startup";
public const string NEW_USER = "newUser";
public const string DELETE_USER = "deleteUser";
public const string CANCEL_SELECTED = "cancelSelected";
public const string USER_SELECTED = "userSelected";
public const string USER_ADDED = "userAdded";
public const string USER_UPDATED = "userUpdated";
public const string USER_DELETED = "userDeleted";
public const string ADD_ROLE = "addRole";
public const string ADD_ROLE_RESULT = "addRoleResult";
}
在例子中,用EventsEnum.STARTUP唤起的时候,PureMVC会实例化一个StartupCommand类的实例,并且执行其Execute方法,在执行完之后就会销毁这个StartupCommand类的实例,PureMVC框架中的任意一个继承自Command的类也是如此。
StartupCommand中,Execute方法里面进行了UserListMediator和UserFormMediator的初始化及注册,注意在UserListMediator和UserFormMediator这两个继承自Mediator的类初始化的时候传进了实例,实例是Mediator需要持有的视图类。
简单看了StartupCommand后再回头来看看UserProxy
//[lzh]
using UnityEngine;
using System.Collections.Generic;
using PureMVC.Patterns;
using PureMVC.Interfaces;
public class UserProxy : Proxy, IProxy
{
public new const string NAME = "UserProxy";
///
/// Return data property cast to proper type
///
public IList<UserVO> Users
{
get {
return (IList<UserVO>) base.Data; }
}
public UserProxy()
: base(NAME, new List<UserVO>())
{
Debug.Log("UserProxy()");
// generate some test data
AddItem(new UserVO("lstooge", "Larry", "Stooge", "[email protected]", "ijk456", "ACCT"));
AddItem(new UserVO("cstooge", "Curly", "Stooge", "[email protected]", "xyz987", "SALES"));
AddItem(new UserVO("mstooge", "Moe", "Stooge", "[email protected]", "abc123", "PLANT"));
AddItem(new UserVO("lzh", "abc", "def", "[email protected]", "abc123", "IT"));
}
///
/// add an item to the data
///
///
public void AddItem(UserVO user)
{
Users.Add(user);
SendNotification(EventsEnum.USER_ADDED, user);
}
///
/// update an item in the data
///
///
public void UpdateItem(UserVO user)
{
for (int i = 0; i < Users.Count; i++)
{
if (Users[i].UserName.Equals(user.UserName))
{
Users[i] = user;
break;
}
}
SendNotification(EventsEnum.USER_UPDATED, user);
}
///
/// delete an item in the data
///
///
public void DeleteItem(UserVO user)
{
for (int i = 0; i < Users.Count; i++)
{
if (Users[i].UserName.Equals(user.UserName))
{
Users.RemoveAt(i);
break;
}
}
SendNotification(EventsEnum.USER_DELETED);
}
}
在UserProxy的构造方法中,调用了基类的构造函数并且传了两个参数
public new const string NAME = "UserProxy";
base(NAME, new List<UserVO>())
这是pureMVC的惯例写法,第二个参数是这个userProxy的数据类,会在父类Proxy里面用object类型的变量持有,所有继承自Proxy的类都应该持有属于其的数据类的实例,数据类实例只存储数据,没有任何关于数据的操作 。数据类可以是任何类型的变量,这里是List
类型的。也因为这样,要获取Proxy的数据类要自己像下面代码一样写个get的方法,框架是不推荐外部直接获取Proxy所持有的数据类实例的 UserVO
就是个简单的用户数据封装,这里就不介绍了。
public IList<UserVO> Users
{
get {
return (IList<UserVO>) base.Data; }
}
现在来看第一个参数,因为每个proxy和mediator类的实例都是只有一个的。第一个NAME参数是用于实现通过ApplicationFacade得到这个UserProxy的实例的标记,NAME 是个常量,可以通过类名而不是实例名找到它。
下面的代码可以得到UserProxy的实例
userProxy = Facade.RetrieveProxy(UserProxy.NAME) as UserProxy;
这段代码经常出现在继承了Mediator以及Command类的类中,开发者用这种写法来直接获得Proxy;
Mediator以及Command都可以获取需要的Proxy对象直接进行数据操作
Mediator以及Command类都继承自INotifier类,这个类持有Facade变量,指向的是全局只有一个的ApplicationFacade的实例。
在UserProxy中,其他的都是开发者自己写的类似增删改查的方法,
上文中的增删改方法最后都有SendNotification,这些增删改的方法对数据造成改变之后,需要调用SendNotification去通知那些对这些增删改数据感兴趣并注册了的Mediator实例,感兴趣的Mediator实例被通知之后会有所行动,如更新数据在视图中的显示等。
再来看看UserListMediator
using UnityEngine;
using System.Collections;
using PureMVC.Patterns;
using PureMVC.Interfaces;
using System.Collections.Generic;
public class UserListMediator : Mediator, IMediator
{
private UserProxy userProxy;
public new const string NAME = "UserListMediator";
private UserList View
{
get {
return (UserList)ViewComponent; }
}
public UserListMediator(UserList userList)
: base(NAME, userList)
{
Debug.Log("UserListMediator()");
userList.NewUser += userList_NewUser;
userList.DeleteUser += userList_DeleteUser;
userList.SelectUser += userList_SelectUser;
}
public override void OnRegister()
{
Debug.Log("UserListMediator.OnRegister()");
base.OnRegister();
userProxy = Facade.RetrieveProxy(UserProxy.NAME) as UserProxy;
View.LoadUsers(userProxy.Users);
}
void userList_NewUser()
{
UserVO user = new UserVO();
SendNotification(EventsEnum.NEW_USER, user);
}
void userList_DeleteUser()
{
SendNotification(EventsEnum.DELETE_USER, View.SelectedUserData);
}
void userList_SelectUser()
{
Debug.Log(" UserListMediator userList_SelectUser ");
SendNotification(EventsEnum.USER_SELECTED, View.SelectedUserData);
}
public override IList<string> ListNotificationInterests()
{
IList<string> list = new List<string>();
list.Add(EventsEnum.USER_DELETED);
list.Add(EventsEnum.CANCEL_SELECTED);
list.Add(EventsEnum.USER_ADDED);
list.Add(EventsEnum.USER_UPDATED);
return list;
}
public override void HandleNotification(INotification notification)
{
switch(notification.Name)
{
case EventsEnum.USER_DELETED:
View.Deselect();
View.LoadUsers(userProxy.Users);
break;
case EventsEnum.CANCEL_SELECTED:
View.Deselect();
break;
case EventsEnum.USER_ADDED:
View.Deselect();
View.LoadUsers(userProxy.Users);
break;
case EventsEnum.USER_UPDATED:
View.Deselect();
View.LoadUsers(userProxy.Users);
break;
}
}
}
在官方文档中,Mediator实例可以持有Proxy实例。这里UserListMediator实例持有了UserProxy实例,再看看构造函数
public UserListMediator(UserList userList)
: base(NAME, userList)
{
Debug.Log("UserListMediator()");
userList.NewUser += userList_NewUser;
userList.DeleteUser += userList_DeleteUser;
userList.SelectUser += userList_SelectUser;
}
写法有些像Proxy,也是pureMVC的惯例写法,第二个参数是这个UserListMediator的视图类,会在父类Mediator里面用object类型的变量持有,所有继承自Mediator的类都应该持有属于其的视图类的实例,视图类实例只负责持有操作视图的相关方法,没有任何关于视图响应时应该处理的功能逻辑的操作 。视图类可以是任何类型的变量,这里是UserList
类型的。也因为这样,要获取Mediator的视图类要自己像下面代码一样写个get的方法,当然这个获取只能是私有的。
private UserList View
{
get {
return (UserList)ViewComponent; }
}
现在来看第一个参数,因为每个proxy和mediator类的实例都是只有一个的。第一个NAME参数是用于实现通过ApplicationFacade得到这个UserListMediator的实例的标记,NAME 是个常量,可以通过类名而不是实例名找到它。
下面的代码可以得到UserProxy的实例
userProxy = Facade.RetrieveMediator(UserListMediator.NAME) as UserProxy;
这段代码经常出现在继承了Command类的类中,开发者用这种写法来直接获得Mediator,Command类一般会直接获得Proxy 和 Mediator直接进行操作。
在继承自Mediator类的实例注册的时候,会调用OnRegister方法,UserListMediator的OnRegister方法获取了UserProxy此外还将其数据加载到了UserList所持有的列表视图中
public override void OnRegister()
{
Debug.Log("UserListMediator.OnRegister()");
base.OnRegister();
userProxy = Facade.RetrieveProxy(UserProxy.NAME) as UserProxy;
View.LoadUsers(userProxy.Users);
}
来看看 里面的ListNotificationInterests方法
public override IList<string> ListNotificationInterests()
{
IList<string> list = new List<string>();
list.Add(EventsEnum.USER_DELETED);
list.Add(EventsEnum.CANCEL_SELECTED);
list.Add(EventsEnum.USER_ADDED);
list.Add(EventsEnum.USER_UPDATED);
return list;
}
这个方法也是重写Mediator的方法,这个方法主要是添加感兴趣的通知,返回的list会由PureMVC机制进行处理。
再看看 里面的 HandleNotification方法
public override void HandleNotification(INotification notification)
{
switch(notification.Name)
{
case EventsEnum.USER_DELETED:
View.Deselect();
View.LoadUsers(userProxy.Users);
break;
case EventsEnum.CANCEL_SELECTED:
View.Deselect();
break;
case EventsEnum.USER_ADDED:
View.Deselect();
View.LoadUsers(userProxy.Users);
break;
case EventsEnum.USER_UPDATED:
View.Deselect();
View.LoadUsers(userProxy.Users);
break;
}
}
这个方法对应的是ListNotificationInterests里面添加的通知,功能是如果监听的通知发生了之后,这个函数就会调用,并且对对应的监听执行对应的操作。 举个简单的例子说明其作用:在调用
SendNotification(EventsEnum.USER_UPDATED);
的时候会执行这个HandleNotification方法的case EventsEnum.USER_UPDATED分支里面的操作。
官方文档不建议一个HandleNotification处理的case超过5个
再看看UserListMediator的构造函数
userList.NewUser += userList_NewUser;
userList.DeleteUser += userList_DeleteUser;
userList.SelectUser += userList_SelectUser;
里面的这段代码用了代理的写法,分别对应了UserList视图的点击增加用户按钮的回调,点击删除用户按钮的回调和选中某项的回调,在PureMVC中,视图和其对应的Mediator使用的交互方式一般是:视图开放代理,Mediator去注册对应的代理
看到这里 就不详细介绍 UserList UserForm 以及 UserFormMediator了,因为前两个只是简单的视图组件,后一个结构和UserListMediator大同小异,介绍UserListMediator已经足够了解Mediator了,接下来通过一个UI响应看看Mediator之间是怎么交互的,
userList_NewUser是UserListMediator对 UserList点击了新建用户的按钮的注册,来看看userList_NewUser里面做了什么
void userList_NewUser()
{
UserVO user = new UserVO();
SendNotification(EventsEnum.NEW_USER, user);
}
发送了一个通知,将UserVo作为通知的参数。
在Visual Studio中光标定位在EventsEnum.NEW_USER的NEW_USER上面,按下Shift+F12,看看哪个地方对这个进行了监听,如图所示:
可以发现在UserFormMediator里面对这个通知进行了监听,看看监听处理的内容,
public override void HandleNotification(INotification note)
{
Debug.Log("UserFormMediator HandleNotification ");
UserVO user;
switch (note.Name)
{
case EventsEnum.NEW_USER:
user = (UserVO)note.Body;
View.ShowUser(user, UserFormMode.ADD);
break;
...
它通过获取note.Body这个参数并强转成UserVO类型来获得SendNotification时候的传的user参数,这是惯例写法,在获得user之后,UserFormMediator将其传给了自己的视图UserForm,并让其将这个user显示出来。
其他的点击事件或者数据更改的运行流程也都是这样。这里就不再详述。
这里再额外说一下MacroCommand。
MacroCommand 让你可以顺序执行多个 Command。每个执行都会创建
一个 Command 对象。
MacroCommand 在构造方法调用自身的 initializeMacroCommand方法。需重写这个方法,调用 addSubCommand 添加子 Command。在这个方法中任意组合 SimpleCommand 和 MacroCommand 来达到目的。MacroCommand 执行的时候会按添加的顺序执行子Command并将唤起时候的Notification参数传进去
对于同一个通知,
每个Mediator对其注册的顺序也就是每个Mediator的ListNotificationInterests调用顺序,决定了通知发生时,Mediator对通知的响应顺序。 可以理解为对通知的响应顺序是Mediator的初始化和注册的顺序决定的
Facade.RegisterMediator(new UserListMediator(mainUI.userList));
Facade.RegisterMediator(new UserFormMediator(mainUI.userForm));
Mediator类的对象需要持有其需要的Proxy类的对象以便直接更改数据
Mediator处理内容包括直接调用proxy的某个方法来通知proxy更改数据,通过Command来进行与其他Mediator的交互,对Notification进行监听来进行与自己相关的视图逻辑处理,直接改变对应视图的状态,发送Notification
Command 类是无状态的,只在需要时才被创建。在执行完Execute方法后便会被销毁,所以变量一般不引用Command的实例
Command 可以获取 Proxy 对象并与之交互,发送 Notification,执行其他的 Command。经常用于复杂的或系统范围的操作
当用 View 注册 Mediator 时,Mediator 的 listNotifications 方法会被调用,
以数组形式返回该 Mediator 对象所关心的所有 Notification。之后,当系统其它角色发出同名的 Notification(通知)时,关心这个通知的Mediator 都会调用 handleNotification 方法并将 Notification 以参数传递到方法。
Proxy发送,但不接收Notification,如果让 Proxy 也侦听 Notification(通知)会导致它和 View(视图)层、Controller(控制)层的耦合度太高。
一般地,实际的应用程序都有一个 Façade 子类,这个 Façade 类对象负责先初始化所有的Proxy,然后初始化并注册所有的Command,其中一个Command是用来来注册所有的Mediator。
PureMVC的惯例的启动写法是:Facade 子类应该写有一个StartUp方法,这个StartUp方法一般是调用注册所有Mediator的Command,应用程序调用 Facade 子类的 Startup 方法,并传递应用程序自身的一个引用来让这个Command能注册所有的Mediator,这样做使得应用程序不需要过多了解 PureMVC,这种写法一般要求这个引用持有所有的视图组件类以便Mediator进行初始化。
继承自Facade的类的单例应该保存在Facade的m_instance变量里面,这样继承自Notifier的类才能通过Facade变量引用到,这时PureMVC框架内部写好的。
Notification使用到的通知字符串最好是由字符串常量负责,这些字符串常量集中定义在一个类里面,使用字符串常量避免了临时写字符串带来的错误风险,这种错误出现了排查起来通常需要较久的时间。
Mediator只负责对自己持有的视图的操作逻辑 简单的更改Proxy的逻辑以及监听Notification和在合适的时候发送Notification,Mediator可以保存一个或多个视图类,其对应的视图类只负责给他的Mediator提供视图响应时候的数据和接口以及更改视图显示的接口。
Mediator持有的视图类不能过大也不能过小,可以把一组相关的视图组件放在一个视图类里,不应该让Mediator的handleNotification方法负责复杂逻辑。业务逻辑应该放在 Command 中而非在 Mediator 中,一般一个 Mediator(handleNotification 方法)处理的Notification 应该在 4、5 个之内。
Mediator 的职责应该要细分。如果处理的 Notification 很多,则意味着 Mediator 需要被拆分,在拆分后的子模块的 Mediator 里处理要比全部放在一起更好。
Mediator 对外不应该公布操作视图的函数。而是自己接收Notification 做出响应来实现。
Proxy只负责对自己持有的数据的操作逻辑以及操作过后的发送Notification的逻辑,其对应的数据类只负责持有数据,没有任何对数据的操作以及其他操作。
在开发中,应尽量做到Mediator 依赖于 Proxy,而 Proxy却不依赖于 Mediator 。Mediator 必须知道 Proxy 的数据是什么,但 Proxy 却并不需要知道 Mediator 的任何内容。
在PureMVC中,只有Mediator与其对应的视图类,Proxy与其对应的数据类能够用紧密耦合的写法之外,其他的地方不应出现紧密耦合的写法,例如一个Mediator里面持有另外一个Mediator的引用,如果一个 Mediator 要和其他 Mediator 通信,那它应该发送 Notification 来实现,而不是直接引用这个 Mediator 来操作,更复杂的涉及到多个Mediator或者Proxy的逻辑应该放置在Command里面进行。
在Command中,允许注册、删除 Mediator、Proxy 和 Command,或者检查它们是否已经注册;发送 Notification 通知 Command 或 Mediator 做出响应;获取任意多个 Proxy 和 Mediator 对象并直接操作它们。Mediator 和 Proxy 可以提供一些操作接口让 Command 调用来管理 ViewComponent 和 Data Object,同时对 Command 隐藏具体操作的细节。
Proxy 是有状态的,当状态发生变化时发送 Notification 通知Mediator,将数据的变化反映到视图,因Proxy状态发生变化而发送Notification的操作不应该放置在更改状态的Mediator或者Command。
如果一个 Mediator 有太多的对 Proxy 及其数据的操作,那么,应该把这些代码重构在 Command 内,简化 Mediator,把业务逻辑(Business Logic)移放到Command 上,这样 Command 可以被 View 的其他部分重用,还会实现 Mediator 和 Proxy 之间的松耦合提高扩展性。
Proxy 不监听 Notification,Proxy 并不关心 View的状态。Proxy 提供方法和属性让其它角色更新数据。Proxy 对象不应该通过引用、操作 Mediator 对象来通知系统它的 Data Object(数据对象)发生了改变。 而是通过发送Notification的方法。Proxy 不关心这些 Notification 被发出后会影响到系统
的什么。这样,把 Model 层和系统操作隔离开来,这样当 View 层和 Controller 层被重构时就不会影响到 Model 层,但是Model 层中的改变总会造成 View/Controller 层的一些重构。
关于数据的操作逻辑尽可能放在 Proxy 中实现,这样当两个Mediator需要同一个Proxy的某个对数据操作的时候,Proxy都可以提供给他们,如果放置Mediator就不一样了。
参考链接:
解读PureMVC框架
PureMVC框架解读(上)