第7章 对话与工作空间管理
现在是理解Seam对话模型更多细节的时候了。
从历史的观点上说,Seam“对话”概念来自三个不同的想法:
* 工作空间的想法,我在2002维多利亚政府的一个项目遇到。在这个项目里,我被迫在Struts的顶层实现工作空间管理,我乞求不要再体验那种经历。
*应用程序的乐观语义学事务和存在的基于一个无状态体系结构的框架不能提供有效扩展持久化管理(Hibernate项目真是受够了对LazyInitializationExceptions“延迟初始化异常”的过失处理,其并不是真的Hibernate故障,而是相当的故障源于无状态体系结构如Spring框架和传统J2EE的无状态会话正面模型对极端限制的持久化上下文模型的支持)实现结果的想法。
*工作流任务的想法。
通过统一这些想法并在框架中提供更进一步的支持,我们实现了一个超强的结构,其让我们与以前相比能用很少的代码构建富裕和更有效的应用程序。
7.1. Seam对话模型
迄今为止,我们看过的例子,是利用下面这些原则的很简单的对话模型:
*在申请请求值期间、处理校验、更新模型值、调用应用程序和JSF请求生命周期的渲染响应阶段,总有一个对话上下文活动。
* 在JSF请求生命周期的恢复视窗结束阶段, Seam 尝试恢复任何前面的长运行对话上下文。如果不存在,Seam创建一个新的临时对话上下文。
*当遇到一个@Begin方法,临时对话上下文被提升为一个长运行上下文。
*当遇到一个@End方法, 任何长运行对话上下文被提升为一个临时对话。
* 在JSF请求生命周期的渲染响应阶段结束时,Seam恢复长运行对话上下文的内容或者摧毁一个临时对话上下文的内容。
*任何faces 请求(一个JSF回调)会传播会话上下文。缺省时,非faces 请求(例如,GET请求)不传播对话上下文,但是,有关这些内容可看下面更多信息。
* 如果JSF请求生命周期被重定向省略,Seam显然地存储并恢复当前对话上下文——除非对话通过@End(beforeRedirect=true)已被结束。
在越过JSF 回调和重定向时,Seam显然地传播对话上下文 (包括临时对话上下文)如果你不做任何指定,一个非 faces请求(例如,GET请求)不会传播对话上下文,并且会在一个新的临时的对话中被处理。通常是——但并不总是——想得到的行为。
如果你想越过一个非faces请求时传播一个Seam对话,你需要明确地编码Seam对话id作为一个请求参数:
<a href="main.jsf?conversationId=#{conversation.id}">Continue</a>
或者,更多JSF-ish:
<h:outputLink value="main.jsf">
<f:param name="conversationId" value="#{conversation.id}"/>
<h:outputText value="Continue"/>
</h:outputLink>
如果你使用Seam 标签库,这是等价的:
<h:outputLink value="main.jsf">
<s:conversationId/>
<h:outputText value="Continue"/>
</h:outputLink>
如果你希望在一个回调时对话上下文传播失效,一个相似的技巧被用:
<h:commandLink action="main" value="Exit">
<f:param name="conversationPropagation" value="none"/>
</h:commandLink>
如果你使用Seam 标签库,这是等价的:
<h:commandLink action="main" value="Exit">
<s:conversationPropagation type="none"/>
</h:commandLink>
注意,让对话上下文传播失效是完全不同于对话结束。
conversationPropagation 请求参数,或者<s:conversationPropagation> 标签,甚至能被使用在开始和结束对话,或者开始一个嵌套对话。
<h:commandLink action="main" value="Exit">
<s:conversationPropagation type="end"/>
</h:commandLink>
<h:commandLink action="main" value="Select Child">
<s:conversationPropagation type="nested"/>
</h:commandLink>
<h:commandLink action="main" value="Select Hotel">
<s:conversationPropagation type="begin"/>
</h:commandLink>
<h:commandLink action="main" value="Select Hotel">
<s:conversationPropagation type="join"/>
</h:commandLink>
这种对话模型使构建恰当的涉及多窗器操作的行为的应用程序很容易。对多数应用程序,这是足够了。一些复杂的应用程序有下面一个或两个附加要求:
* 一个对话跨多个小的用户界面单元,连续或者甚至并发执行的单元。小单元嵌套的对话有它们自己独立的对话状态,并且也访问外部对话的状态。
* 在同一个游览器窗器内,用户能在许多对话之间切换。这个特色被称为工作空间管理。
7.2. 嵌套的对话
一个嵌套对话被创建,通过在一个存在的对话范围内调用@Begin(nested=true) 标记的一个方法。一个嵌套对话有它自己的对话上下文,并且也能只读访问外部对话的上下文(它能读外部的对话的上下文变量,但不能写它们)。当随后遇到一个@End时,嵌套对话将会被摧毁,并且外部对话将会恢复,通过“弹出”对话堆栈。对话可以被嵌套到任意深度。某些用户动作(工作空间管理,或者返回按钮),在内部嵌套结束前,可能引起对话恢复。在这种情况下,有多个并发嵌套对话属于同一个外部对话是可能的。如果在一个嵌套对话结束之前外部嵌套结束,Seam将会随同外部嵌套摧毁所有嵌套对话上下文。
一个对话可以被认为是一个可持续状态。嵌套对话允许应用程序在一个用户界面的不同点捕获一个一致的可持续状态,因而真正地确保在返回按钮和工作空间管理“面”的正确行为
TODO: 一个例子显示了一个嵌套对话,当你用返回按钮时如何预防坏的东西发生。
通常,如果一个组件存在当前嵌套对话的一个父对话中,嵌套对话将使用同一个实例。偶尔,在每一个嵌套对话中有一不同实例是有用的,所以,存在于父对话的组件实例对子对话是不可见。你能通过@PerNestedConversation注释组件达到这种行为。
7.3. 用GET 请求启动对话
当一个非faces请求(例如,一个HTTP GET请求)访问一个页面时,JSF并没有定义被触发的任何一种类型的动作侦听器。如果用户标记了页面,或者如果我们通过<h:outputLink>导航到这个页面,这能发生。
有时候,我们想页面被访问时马上开始一个对话。因为没有JSF动作方法,我们不能用通常的方法解决问题,通过用@Begin注释动作。
如果页面需要取一些状态放进一个上下文变量,一个更深一层的问题发生。我们已经看到两种方法解决这种问题。如果状态维持在一个Seam组件,我们能用一个@Create 方法取得状态,如果没有,我们能为一个上下文变量定义一个@Factory方法。
如果没有这些选项目为你工作,Seam让你在pages.xml中定义一个页面动作。
<pages>
<page view-id="/messageList.jsp" action="#{messageManager.list}"/>
...
</pages>
在渲染响应阶段的开始,这个动作方法被调用,随时页面将要被渲染。如果一个页面返回一个非空结果,Seam会处理任何适当的JSF和Seam导航控制,可能导致一个完全不同的页面被渲染。
如果你想在渲染页面前开始一个对话,你只能使用一个内建动作方法去完成:
<pages>
<page view-id="/messageList.jsp" action="#{conversation.begin}"/>
...
</pages>
注意,你也能从一个JSF调用这个内建动作,并且,相似的,你能使用#{conversation.end}结束对话。
如果你想更多控制,加入存在的对话或开始一个嵌套对话,开始一个页面流或一个原子对话,你应用使用<begin-conversation>元素。
<pages>
<page view-id="/messageList.jsp">
<begin-conversation nested="true" pageflow="AddItem"/>
<page>
...
</pages>
也有一个 <end-conversation> 元素。
<pages>
<page view-id="/home.jsp">
<end-conversation/>
<page>
...
</pages>
为解决第一个问题,我们有五种选项:
*用@Begin注释@Create方法
*用@Begin注释@Factory方法
*用@Begin注释Seam 页面动作
* 使用 <begin-conversation> 在 pages.xml。
* 使用 #{conversation.begin}作为Seam 页面动作方法
7.4.使用<s:link>和<s:button>
JSF命令链接总是通过JavaScript处理一个表单提交,这打破了网页游览器“打开在新窗口”或者“打开在新标签”特色。 在简单JSF中,如果你需要使用这种功能,你需要使用 <h:outputLink>。但是对<h:outputLink>有两个主要限制。
* JSF没有提供方法糸动作侦听器到<h:outputLink> 。
*因为没有实际表单提交,JSF不传播所选的一个DataModel 行。
Seam提供页面动作概念帮助解决第一个问题,但是对第二问题没有帮助。我们能环绕这工作,通过使用传一个请求参数的RESTful 方法和询问在服务器边所选的对象。在一些情况下——例如象Seam blog例子应用程序——这确实是最好的方法。RESTful 式样支持下书签,因为它不要求服务器边状态。在其它情况下,我们不关心书签,@DataModel和 @DataModelSelection的使用正好是这样方便和明晰!
为填补这缺少的功能,和使对话传播在管理上更简单,Seam提供了<s:link>标签。
这个链接可以只指定JSF视窗id:
<s:link view="/login.xhtml" value="Login"/>
或者,它能指定一个动作方法(在那个情况下动作结果决定页面结果):
<s:link action="#{login.logout}" value="Logout"/>
如果指定两者,一个JSF视窗和动作方法,除非动作方法返回了一个非空结果,“视窗”将会被使用:
<s:link view="/loggedOut.xhtml" action="#{login.logout}" value="Logout"/>
链接自动传播所选的一个使用在<h:dataTable>内的DataModel的行
<s:link view="/hotel.xhtml" action="#{hotelSearch.selectHotel}" value="#{hotel.name}"/>
你也能离开一个存在的对话范围:
<s:link view="/main.xhtml" propagation="none"/>
你能开始,结束,或者嵌套对话:
<s:link action="#{issueEditor.viewComment}" propagation="nest"/>
如果链接开始了一个对话,你甚至能指定使用一个页面流:
<s:link action="#{documentEditor.getDocument}" propagation="begin"
pageflow="EditDocument"/>
taskInstance属性,如果为使用jBPM任务列表:
<s:link action="#{documentApproval.approveOrReject}" taskInstance="#{task}"/>
(看DVD Store 演示的例子应用程序。)
最后,如果你需要“link”被渲染成一个按钮,使用<s:button>:
<s:button action="#{login.logout}" value="Logout"/>
7.5.成功消息
显示一条消息给用户指示一个动作的成功或失败是相当普遍。为这使用JSF FacesMessage很方便的。不幸的,一个成功的动作常要求一个游览器重定向,并且JSF越过重定向不传播faces消息。这使得在简单JSF中显示消息十分困难。
内建在对话范围的名为facesMessages Seam组件解决了这个问题。
(你必须安装了Seam 过滤器)
@Name("editDocumentAction")
@Stateless
public class EditDocumentBean implements EditDocument {
@In EntityManager em;
@In Document document;
@In FacesMessages facesMessages;
public String update() {
em.merge(document);
facesMessages.add("Document updated");
}
}
任何增加到facesMessages 消息被使用在当前对话的下一次渲染阶段。这当没有长运对话时甚至也能工作,因为Seam 保护一致性临时对话上下文越过重定向。
你甚至能在一个faces消息汇总中包括JSF EL表达式:
facesMessages.add("Document #{document.title} was updated");
你可以用普通的方法显示消息,例如:
<h:messages globalOnly="true"/>
7.6. 自然对话ids
当用对话处理持久化对象进行工作时,使用对象的自然业务关键字替代标准的是令人想要的,“代理”对话id:
容易重定向到存在的对话
如果用户请求同样的操作两次,重定向到一个存在的对话是有益的。举这样的例子:“你在ebay,半途通过一个正好赢得的付款项目,当为你的双亲一个圣诞礼物。按理说你会直接发送给他们——你进入你的支付详细资料,但是你忘记了他们的地址。你偶然地重用同样的浏览器查找出他们的地址。现在你需要返回,支付这个项目”
用一个自然对话让用户重新加入到存在的对话真是容易的,并获得他们离去的地方——正好获得他们重新加入的带有项目id的支付项目对话作为对话id。
使用友好的URL
对我而言,这由一个导航层级(我能通过编辑url来导航)和一个有目的的url(象这个Wiki的用法——那样不使用随机ids确定事物)构成。当然,对一些应用程序用户友好的URL是不重要的。
用一个自然对话,当你构建你的hotel booking系统时(或者,当然,无论你的应用程序是哪一种),你能产生一个URL,如http://seam-hotels/book.seam?hotel=BestWesternAntwerpen(当然,hotel映射到你的领域模型的无论哪个参数必须是唯一的),并且用URLRewrite(URL重写)容易转换这到http://seam-hotels/book/BestWesternAntwerpen。
太好了!
7.7. 创建一个自然对话
自然对话被定义在pages.xml:
<conversation name="PlaceBid"
parameter-name="auctionId"
parameter-value="#{auction.auctionId}"/>
在上面定义中要注意的第一件事是对话有一个名字,在这种案例中的PlaceBid。这个名字唯一地标识这特殊的命名对话,并且通过页面定义标识一个参与的命名对话被使用。下一个属性, parameter-name 定义了会包含自然对话id的请求参数, 代替缺省的对话id参数。在这个例子, parameter-name 是auctionId。 这意谓一个出现在你的页面的URL中的对话参数如cid=123,它会被auctionId=765432 替代。
在上面配置中的最后一个属性parameter-value,定义了一个EL表达式,产生自然业务关键字的值,用来作为对话id。在这个例子中,对话id会是当前范围的auction实例的主关键的值。
在下面,我们定义的页面会传播命名对话。通过对一个页面定义指定对话属性来实现:
<page view-id="/bid.xhtml" conversation="PlaceBid" login-required="true">
<navigation from-action="#{bidAction.confirmBid}">
<rule if-outcome="success">
<redirect view-id="/auction.xhtml">
<param name="id" value="#{bidAction.bid.auction.auctionId}"/>
</redirect>
</rule>
</navigation>
</page>
7.8.重定向到一个自然对话
当启动,或重定向到一个自然对话时,为指定自然对话的名字有一些选项。让我们从看看下面的页面定义开始:
<page view-id="/auction.xhtml">
<param name="id" value="#{auctionDetail.selectedAuctionId}"/>
<navigation from-action="#{bidAction.placeBid}">
<redirect view-id="/bid.xhtml"/>
</navigation>
</page>
从这里,从我们的auction拍卖视窗,我们能看到那个调用动作#{bidAction.placeBid}(用这种方法,所有这些例子取自Seam中的seamBay例子),我们会重定向到/bid.xhtml,那个,如我们前看的,是用自然对话PlaceBid配置的。对我们动作方法的声明看起来如这样:
@Begin(join = true)
public void placeBid()
当命名对话在<page/>元素中被指定时,在动作方法已经被调用后,对命名对象的重定向,象导航控制的部分一样发生。 当重定向到一个存在的对话这是一个问题,因为重定向需要发生在动作方法被调用前。因而,当动作被调用时,指定对话名是必要的。做这个的一个方法是通过用s:conversationName标签:
<h:commandButton id="placeBidWithAmount" styleClass="placeBid"
action="#{bidAction.placeBid}">
<s:conversationName value="PlaceBid"/>
</h:commandButton>
另一个选择是指定conversationName属性,当使用s:link 或s:button:
<s:link value="Place Bid" action="#{bidAction.placeBid}" conversationName="PlaceBid"/>
7.9. 工作空间管理
工作空间管理有能力在一个单一窗口切换对话。Seam使得工作空间管理在Java代码级完全透明。为了能用工作空间管理,你需要做:
*为每一个视窗id(当使用JSF或者Seam导航控制时)或者页面节点(当使用jPDL页面流时)提供描述文本。这个描述文本通过工作空间切换器显示给用户。