public class timemodel {
public static final int default_period = 60;
private timer timer;
private boolean running;
private int period;
private int seconds;
private propertychangesupport propsupport;
/**
* getters and setters for model properties.
*/
/**
* returns the number of counted seconds.
*
* @return the number of counted seconds.
*/
public int getseconds() {
return seconds;
}
/**
* sets the number of counted seconds. propsupport is an instance of propertychangesupport
* used to dispatch model state change events.
*
* @param seconds the number of counted seconds.
*/
public void setseconds(int seconds) {
propsupport.firepropertychange("seconds",this.seconds,seconds);
this.seconds = seconds;
}
/**
* sets the period that the timer will count. propsupport is an instance of propertychangesupport
* used to dispatch model state change events.
*
* @param period the period that the timer will count.
*/
public void setperiod(integer period){
propsupport.firepropertychange("period",this.period,period);
this.period = period;
}
/**
* returns the period that the timer will count.
*
* @return the period that the timer will count.
*/
public int getperiod() {
return period;
}
/**
* decides if the timer must restart, depending on the user answer. this method
* is invoked by the controller once the view has been notified that the timer has
* counted all the seconds defined in the period.
*
* @param answer the user answer.
*/
public void questionanswer(boolean answer){
if (answer) {
timer = new timer();
timer.schedule(new secondstask(this),1000,1000);
running = true;
}
}
/**
* starts/stop the timer. this method is invoked by the controller on user input.
*/
public void settimer(){
if (running) {
timer.cancel();
timer.purge();
}
else {
setseconds(0);
timer = new timer();
timer.schedule(new secondstask(this),1000,1000);
}
running = !running;
}
/**
* the task that counts the seconds.
*/
private class secondstask extends timertask {
/**
* we're not interested in the implementation so i omit it.
*/
}
}
controller只定义了用户可以执行的并且能够从下列接口抽象出来的actions。
public interface timecontroller {
/**
* action invoked when the user wants to start/stop the timer
*/
void userstartstoptimer();
/**
* action invoked when the user wants to restart the timer
*/
void userrestarttimer();
/**
* action invoked when the user wants to modify the timer period
*
* @param newperiod the new period
*/
void usermodifyperiod(integer newperiod);
}
你可以使用你自己喜欢的gui编辑器来画这个view。出于我们自身的情况,我们只需要几个公共的methods就可以提供足够的功能来更新view的fields,如下面的这个例子所示:
/**
* updates the gui seconds fields
*/
public void setscnfld(integer sec){
// scnfld is a swing text field
swingutilities.invokelater(new runnable() {
public void run() {
scnfld.settext(sec.tostring());
}
});
}
在这里我们注意到我们正在使用pojos (plain-old java objects),同时我们不用遵守任何编码习惯或者实现特定的接口(事件激发代码除外)。剩下的就只有定义组件之间的绑定了。
事件分派annotations
绑定机制的核心就是@modeldependent annotation的定义:
@retention(retentionpolicy.runtime)
@target(elementtype.method)
public @interface modeldependent {
string modelkey() default "";
string propertykey() default "";
boolean runtimemodel() default false;
boolean runtimeproperty() default false;
}
这个annotation能被用在view的methods上,同时dispatcher也会使用这些提供的参数(即modelkey和propertykey)来确定这个view将会响应的model事件。这个view既使用modelkey参数来指定它感兴趣的可利用的models又使用propertykey参数来匹配分配的java.beans.propertychangeevents的属性名称。
view method setscnfld()因此被标注以下信息(这里,timemodel提供了用来将model注册到dispatcher上的key):
/**
* updates the gui seconds fields
*/
@modeldependent(modelkey = "timemodel", propertykey = "seconds")
public void setscnfld(final integer sec){
// scnfld is a swing text field
swingutilities.invokelater(new runnable() {
public void run() {
scnfld.settext(sec.tostring());
}
});
}
由于dispatcher既知道model激发的事件又知道事件本身-例如,它知道关联的modelkey和propertykey-这是唯一需要用来绑定views和models的信息。model和view甚至不需要分享通信接口或者共用的数据库。
借助我们讨论的绑定机制,我们可以轻易的改变潜在的view而不改变其他任何东西。下面的代码是按照使用swt(standard widget toolkit)而不是swing实现的同一个method:
@modeldependent(modelkey = "timemodel", propertykey = "seconds")
public void setscnfld(final integer sec){
display.getdefault().asyncexec(new runnable() {
public void run() {
secondsfield.settext(sec.tostring());
}
});
}
一个完全没有耦合的系统存在以下优点:view可以更加容易地适应model地改变,尽管model通常都是稳定地,相反view是经常被改变。加上系统可以通过使用gui编辑器或者其他源码生成器来设计,避免了将生成地代码与model-view通信代码混合在一起。又由于model-view的绑定信息是和源码关联的元数据,于是也相对容易把它应用到ide生成的guis或者将已经存在的应用程序转化成这个框架。加之拥有单独的基础代码,view和model可以被当作是独立组件来开发,这很可能简化了应用程序的开发过程。组件测试也可以被简化,因为每个组件可以被单独地测试,并且出于调试的目的,我们可以用假的model和view来代替真实的组件。
然而,这里也存在许多缺点。因为现在当使用接口和公共的classes来绑定model和view时,我们不能再提供编译时期的安全性了,可能出现的打字错误将导致组件之间一个绑定的遗漏,从而导致出现运行时期的错误。
通过使用@modeldependent的讨论过的modelkey和propertykey元素,你可以定义model和view之间静态的联系。然而,现实世界的应用程序证明view必须能够经常动态的适应变化的models和应用程序的状态:考虑到用户界面的不同部分能够在应用程序的生命周期内被创造和删除。因此我将介绍怎么使用这个框架与其他常用技术一起来处理此类情形。
动态mvc绑定
对于那些依赖xml绑定(或者其他一些基于配置文件的声明性绑定)的框架,存在一个问题那就是静态绑定规则。在这些框架下,动态变化是不可能的,于是通常开发者决定每次将冗余的绑定信息与一些使用正确绑定的判定算法耦合在一起。
为了巧妙的解决这个问题,stamps框架提供了两种方式在运行时期改变绑定。 第一种方式是,views和models可以采用事件监听器与gui窗口小部件联合的方式在dispatcher上注册和注销。这样允许特定的views只在需要他们的时候被通知到。例如,一个与应用程序有联系的监视控制台可以只在用户请求的时候与被它监视的对象绑定在一起。
第二种方式是利用@modeldependent annotation提供的两个元素runtimemodel() 和 runtimeproperty()。他们指明了某个确定的model和它的分配事件会在运行时期被确定。如果这两个设定中有一个是正确的,那么各自的key(modelkey 或propertykey)会在view上被method调用来得到需要使用的值。例如:一个负责显示一组新channels (每个channel就是一个model)的view,它就依赖于用户的输入来确定需要绑定的channel。
这种情形的实例如下:
// this method is invoked to display all the messages of one news channel
@modeldependent(modelkey = "dynamicchannel", propertykey = "allmessages" , runtimemodel = true)
public void setallmessages(java.util.list messages) {
// updates the user interface
}
public string getdynamicchannel() {
// returns the channel requested by the user
}
附加的annotations
由于世界并不完美,一些附加的annotations被定义来帮助解决现实世界的案例。@namespace允许开发者为了更好的管理model domain将其再细分成不同的部分。由于单独一个dispatcher可以处理多个models,model keys中将出现的冲突。因此,它能将成群的models和相关的views分到不同的但同属一个namespace下的domains中去, 这样一来,他们就不会干扰对方。
@transform annotation提供了on-the-fly对象转化, 从包含在model事件中的对象到被receiving views接受的对象的。因而,这个框架就可以适应已存的代码而不需要做任何的改动。这个annotation接受一个注册在有效转化上的单一参数(被定义成一个特殊接口的实现)。
@refreshable annotation能通过标注model的属性来支持前面讨论的动态连接和分离views。使用这个annotation,该框架可以处理静态和动态的mvc布局,在不同的时间把不同的views绑定到model上。
要理解@refreshable的使用,我们必须回到之前的那个监控控制台的例子。这个控制台(用mvc的术语来说就是一个view)可以动态地绑定和离开model,取决于用户的需要。当控制器连接到model的时候@refreshable annotation可以被用来让这个控制器随时了解其model的状态。当一个view连接到这个框架时,它必须在当前model的状态下被更新。因此,dispatcher扫描model寻找@refreshable annotations并且生成与view它本身从model普通接受到的相同的事件。这些事件接着被之前讨论过的绑定机制分派。
分布式mvc网络
dispatcher有一个很重的负担那就是它负责处理事件的传送周期中所有重型信息的传递:
· model激发一个事件用来确定它已经经历过的一些改变, dispatcher处理通知model.
· dispatcher扫描所有注册在它那里的views, 寻找@modeldependent annotations, 这些annotations明确了views希望通知的改变及当每个model事件发生时,需要在views上调用的method.
· 如果需要,转化将会被用于事件数据上.
· view method在被调用时会从被激发的事件里抽取参数,接着view会更新自己.
从另一个方面来讲,当一个新view在dispatcher上注册时:
· view告诉dispatcher有关modelkey的信息,modelkey能确定它将被连接到哪一个model上(该model的事件将负责组装view)
· 如果需要,dispatcher扫描model寻找@refreshable annotations并使用他们来生产将要及时更新view假的model事件
· 这些事件将通过使用上述的顺序被分派, 接着view被更新.
所有这些既不涉及view也不涉及model的工作,他们站在他们各自的信息通信渠道的两端.无所谓这些信息是在一个本地jvm内部传输还是在多个远程主机上的jvm之间传输.如果想将本地应用程序转化成client/server应用程序所需的只是简单地改变dispatcher里面的逻辑,而model和view都不会受影响.下图就是一个示例:
图4. 一个基于分布式网路建立的mvc,点击缩略图查看全图
如上图所示,单一的dispatcher被一个与model处在同一个host上的transmitter(it.battlehorse.stamps.impl.broadcastdispatcher的一个instance)和一个(或多个) 与view处在同一个host上的receiver(it.battlehorse.stamps.impl.funneldispatcher)所取代. stamps 框架默认的实现使用了一个创建于jgroups上的消息传送层, jgroups是一个可靠的多点传送通信的工具包,象网络传输机制(但是不同的实现和使用)一样工作. 通过使用它可以获得一个稳定可靠的, 多协议的, 失败警觉的通信.
对我们应用程序(dispatcher)初步建立的一个改变, 使我们从一个单一用户界面的独立运行的应用程序转移到一个多用户分布式的应用程序.当model进入或离开这个网络(想象一个通信失败)的时候,框架可以通知无数的监听接口, 于是远程views可以采取适当的响应.例如,显示一个警告信息给用户. 这个框架也可以提供有用的methods来帮助将本地的controllers转化成远程的.
总结和摘要
仍有许多元素需要被探索,就像设计controllers的方式一样,它在目前和dispatchers具有一致的普遍性.该框架假设普通的controller-model绑定,由于前者需要知道如何去驱动后者.未来的开发方向将是支持不同类型的views,例如使用一个web浏览器, 网络警觉的applets,和java与javascript的通信.
已经讨论的stamps库说明如何在一个mvc架构中降低views和models之间的耦合以及这个框架可以有效的利用java annotations将绑定信息从实际开发程序组件分离开.拥有隔离的绑定逻辑允许你在物理上将元件分离开并且能提供一个本地和一个client/server结构而不需要改变应用逻辑或者表示层. 这些目标提供对由一个象mvc一样坚固的设计模式与由annotations提供的功能强大的元数据结合在一起所提供的可能性的洞察.
新闻热点
疑难解答