为了保障原创作者在本站发表文章的利益, 并维护本站原创的精神, 特声明: RIAShanghai对有以下任何情况之一的文章将不通知作者并直接进行快意删除:
- 非原创, 或者原创但一文多发;
- 各种形式的广告与吹擂;
- 不符合本站文章格式.
欢迎各位读者监督. 谢谢合作. 另: 作为Adobe正式的UG, 我们将把Adobe不定期分发的软件,书籍及各种纪念品赠送给发文活跃的作者, 共同进步.
Pattern name 模式名: Cancellable Selection Classification 分类: Behavioral
Motivation: 在多对象共享同一个编辑器(editor)或者在任何时间仅允许一个编辑器的情况下, 如果当前对象尚未保存, 界面不应该直接转换到新的对象 - 至少应该给用户警告并提示选择的机会. 例如: 多个tabs, 每个tab页面有可编辑的对象; 一个tree, table或者list上显示所有文件, 旁边为唯一共享的编辑器.
Approach:
将selection明确分成两个阶段 - 一是试图选择的阶段, 此阶段的任务是询问所有listeners是否同意选择; 如果第一阶段一致同意, 第二阶段才是实施真正的选择.
Implementation (Flex):
import mx.controls.Tree;
import mx.core.EventPriority;
import mx.events.ListEvent;
/**
* Dispatched when a selecting is attempted and also when it is confirmed.
* @eventType com.insprise.common.ui.EventCancellableSelection.EVENT_EVENT_CANCELLABLE_SELECTION
*/
[Event(name="eventCancellableSelection", type="com.insprise.common.ui.EventCancellableSelection")]
/**
* A tree that supports cancellable node selection.
* You should use property currentItem to retrieve the confirmed selected item.
* Events:
* add listeners to EventCancellableSelection to cancel default or monitor initial selecting when its kind is SELECTING;
* and retrieve current value when its kind is SELECTED.
*/
public class TreeCancellableSelect extends Tree {
protected var _currentItem:*;
// Constructor.
public function TreeCancellableSelect() {
super();
addEventListener(ListEvent.CHANGE, onTreeSelChange);
addEventListener(EventCancellableSelection.EVENT_CANCELLABLE_SELECTION, handleNodeSelecting, false, EventPriority.DEFAULT_HANDLER); // low priority
// Ref: "Creating Default, Cancelable Event Handlers" - http://www.darronschall.com/weblog/2008/01/creating-default-cancelable-e...
}
// Called when user selected a OU from the tree.
protected function onTreeSelChange(event:ListEvent):void {
dispatchEvent(new EventCancellableSelection(true, EventCancellableSelection.KIND_SELECTING, selectedItem, _currentItem)); // cancellable.
}
// Called when EventCancellableSelection.KIND_SELECTING and all other handlers have already been invoked to check if action cancelled.
protected function handleNodeSelecting(event:EventCancellableSelection):void {
if(! event.isKindSelecting()) { // only handle selecting
return;
}
if(event.isDefaultPrevented()) { // cancelled.
selectedItem = _currentItem; // restore
}else{ // ok
doSetCurrentItem(selectedItem);
dispatchEvent(new EventCancellableSelection(false, EventCancellableSelection.KIND_SELECTED, _currentItem, _currentItem));
}
}
/**
* The current selected item (confirmed).
* When set, it dispatches cancellable EVENT_ITEM_SELECTING event; if set is done, it dispatches EVENT_ITEM_SELECTED event.
*/
public function get currentItem():* {
return _currentItem;
}
public function set currentItem(value:*):void {
selectedItem = value;
onTreeSelChange(null);
}
/**
* Directly set the current item.
* A subclass may override this method to handle actual selection - remember to call super.doSetCurrentItem in the first line.
*/
protected function doSetCurrentItem(value:*):void {
this._currentItem = value;
}
} // End class
import flash.events.Event;
/**
* Repesenting a two staging selection with first stage cancellable: stage 1: selecting; stage 2: selected (if stage 1 is not cancelled).
*/
public class EventCancellableSelection extends Event
{
/** Repesenting a two staging selection with first stage cancellable: stage 1: selecting; stage 2: selected (if stage 1 is not cancelled). */
public static var EVENT_CANCELLABLE_SELECTION:String = "eventCancellableSelection";
/** Event dispatched when an selecting attempt is made; cancellable */
public static var KIND_SELECTING:String = "eventSelecting";
/** Event dispatched when selection is confirmed. */
public static var KIND_SELECTED:String = "eventSelected";
/** Either of KIND_SELECTING or KIND_SELECTED */
public var kind:String;
/** The attempted selection */
public var selectingValue:*;
/** The existing value - previously selected value. */
public var existingValue:*;
/** Constructor */
public function EventCancellableSelection(cancelable:Boolean, kind_:String, selectingValue_:*, existingValue_:* = null) {
super(EVENT_CANCELLABLE_SELECTION, bubbles, cancelable);
if(kind_ != KIND_SELECTING && kind_ != KIND_SELECTED) {
throw new UnsupportedError(kind_);
}
this.kind = kind_;
this.selectingValue = selectingValue_;
this.existingValue = existingValue_;
if(kind_ == KIND_SELECTING && !cancelable) {
throw new Error("KIND_SELECTING should be cancelable. ");
}
if(kind_ == KIND_SELECTED && cancelable) {
throw new Error("KIND_SELECTED should not be cancelable. ");
}
}
public function isKindSelecting():Boolean {
return kind == KIND_SELECTING;
}
public function isKindSelected():Boolean {
return kind == KIND_SELECTED;
}
override public function clone():Event {
return new EventCancellableSelection(cancelable, kind, selectingValue, existingValue);
}
override public function toString():String {
return formatToString("EventCancellableSelection", "kind", "selectingValue", "existingValue");
}
} // End class
// Usage:
var tree:TreeCancellableSelect = ...;
tree.addEventListener(EventCancellableSelection.EVENT_CANCELLABLE_SELECTION, onTreeSelection);
/**
* Called when node selection on the tree is attempted or confirmed.
*/
protected function onTreeSelection(event:EventCancellableSelection):void {
if(event.isKindSelecting()) { // attempting.
if(editor.isDirty()) {
// stop the default action and warn the user.
event.preventDefault();
Alert.show("Discard unsaved changes? ", "Warn", Alert.YES|Alert.CANCEL, null, onWarnUnsavedChangesAlertClose).data = event;
}
}else{ // selection changed.
if(event.selectingValue === event.existingValue) { // same, do not care.
return;
}
var myObject:* = event.selectingValue as OrgUnit;
editor.setModel(myObject);
}
}
// 为保存警告关闭响应
protected function onWarnUnsavedChangesAlertClose(e:CloseEvent):void {
if(e.detail != Alert.YES) { // cancel selecting on tree
tree.currentItem = EventCancellableSelection(Alert(e.target).data).existingValue;
return;
}else { // proceeds selections and discard unsaved changes
ouTree.currentItem = EventCancellableSelection(Alert(e.target).data).selectingValue; // will trigger EventCancellableSelection
}
}
Flex's Event.preventDefault()可参考: "Creating Default, Cancelable Event Handlers".
Implementation (Java):
Java实现更为简单, 跟Flex使用特别的asynchronized event dispatching不同, Java就是一般的同线程代码运行. 有两种方法, 一是直接用代码for each listener, call listener.method检查是否每个赞同, 然后再考虑下一步; 而是使用vetoable listeners, 详情可Google 'vetoable listeners java'. 下面为第二中方法启发性的例子, 而非真正的实现:
// Register for property change events on the bean
bean.addVetoableChangeListener(new MyVetoableChangeListener());
class MyVetoableChangeListener implements VetoableChangeListener {
// This method is called every time the property value is changed
public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
// Get the old value of the property
Object oldValue = evt.getOldValue();
// Get the new value of the property
Object newValue = evt.getNewValue();
// Determine if the change should be vetoed, thereby preventing the change
boolean veto = false;
if (veto) {
throw new PropertyVetoException("the reason for the veto", evt);
}
}
}
优点
Cancellable Selection带来的好处是它清晰将选择过程分为两段, 将使用者从必须创建子类解脱出来, 转而允许使用简单的listener. 代码更加清晰.
缺点
Flex的标准Tree/Table等control使用直接选择, 用上面的TreeCancellableSelect需要一定的学习时间 - 需要将习惯的selectItem转而使用currentItem.