04 September 2006

Well,大家都看過 Gmail 在做 Ajax loading 的時候,右上角會出現個紅色的 "Loading..." 的訊息。這個是提升 UI usuability 的一個很好的 pattern,很多 framework 都有提供 (雖然型式有點小不同)。個人比較喜歡用 Gmail 這種方式,一來在右上角的小紅標不會破壞網頁的 layout。二來一般人遇到 browser 停住時,多半會自然的往右上角一看,看看 browser mark 有沒有在轉,放在右上角正好可以捕捉視線。三來這個樣式已經快變 standard 了,使用者學習曲線較低。

不幸的是,Wicket 本身沒有提供這種小紅標,它預設是使用一個小 icon 在按鈕或是 link 旁轉啊轉的。當然這個也很有用啦,只不過有時會破壞 layout,所以不會像小紅標這麼好用。

Ok,我們來看看怎麼加這段 Loading 小紅標。首先先要有一段 javascript 來產生小紅標:

var DisabledZone = new function () {
	var DISABLED_ZONE_ID = "wicket_js_disabled_zone_id";
	var LOADING_MESSAGE_ID = "wicket_js_loading_message_id";
	var showLoadingMessage = null;
	var SHOW_LOADING_AFTER_MINI_SEC = 300;
	function getDisabledZone() {
		return document.getElementById(DISABLED_ZONE_ID);
	}
	function getLoadingMessage() {
		return document.getElementById(LOADING_MESSAGE_ID);
	}

	//disabledZone not work with IE, only loadingMessage take effect!
	this.on = function () {
		var disabledZone = getDisabledZone();
		if (!disabledZone) {
			disabledZone = document.createElement("div");
			disabledZone.setAttribute("id", DISABLED_ZONE_ID);
			disabledZone.style.position = "absolute";
			disabledZone.style.zIndex = "1000";
			disabledZone.style.left = "0px";
			disabledZone.style.top = "0px";
			document.body.appendChild(disabledZone);
			var loadingMessage = document.createElement("span");
			loadingMessage.style.position = "fixed";
			loadingMessage.setAttribute("id", LOADING_MESSAGE_ID);
			loadingMessage.style.zIndex = "1000";
			loadingMessage.style.right = "0px";
			loadingMessage.style.top = "0px";
			loadingMessage.style.background = "#BB0000";
			loadingMessage.style.fontWeight = "bold";
			loadingMessage.style.padding = "0.1em 0.5em 0.1em 0.5em";
			loadingMessage.style.color = "white";
			loadingMessage.style.visibility = "hidden"; // initially hidden
			disabledZone.appendChild(loadingMessage);
			var text = document.createTextNode("Loading...");
			loadingMessage.appendChild(text);
		}
		disabledZone.style.width = document.body.scrollWidth + "px";
		disabledZone.style.height = document.body.scrollHeight + "px";
		disabledZone.style.visibility = "visible";
		var lm = getLoadingMessage();
		//show loading message if loading too long
		showLoadingMessage = window.setTimeout(function () {
			lm.style.visibility = "visible";
		}, SHOW_LOADING_AFTER_MINI_SEC); 		

		//fix position must after width/height change
		repairIEFixedPosition(lm);
	};
	this.off = function () {
		if (showLoadingMessage) {
			window.clearTimeout(showLoadingMessage);
		}
		var disabledZone = getDisabledZone();
		if (disabledZone) {
			var loadingMessage = getLoadingMessage();
			loadingMessage.style.visibility = "hidden";
			disabledZone.style.visibility = "hidden";
			disabledZone.style.width = "0px";
			disabledZone.style.height = "0px";
		}
	};
	function repairIEFixedPosition(el) {
		if (navigator.userAgent.match("IE")) {
			//below require fixed.js to repair IE fix position
			if (!el.fixed_bound) {
				fixed_bind(el);
				el.fixed_bound = true;
			}
		}
	}
};

這段 javascript 採 singleton pattern 寫成。所以 namespace 都集中在 DisabledZone 這個物件裡。要顯示 "Loading..." 時,執行 DisabledZone.on() ; 欲關閉時則執行 DisabledZone.off(); 另外,可以設定延遲 "Loading..." 出現的時間,只要修改 SHOW_LOADING_AFTER_MINI_SEC 即可。上面的設定是使用 300 minisec (0.3 秒)。上面的 javascript 是參考自 DWR 的作法,不過它預設沒有延遲的設定。少了延遲,即使 ajax loading 的很快,小紅標也會出現,然後閃一下就不見了。閃來閃去的結果會讓使用者覺得很煩,所以還是加上去比較好。(據我的觀察,gmail 的 loading message 也是有延遲的。)

OK,上面的 javascript 並不限於 Wicket,其他 framework 也可以拿來用。我們現在來看看要怎麼加到 Wicket 裡。在 Wicket 裡面,每個 ajax 元件基本上都有一個getAjaxCallDecorator() 的 method 可以 override。顧名思義,AjaxCallDecorator 可以讓你 "decorate" Wicket 所產生的 ajax javascript,用來提供額外的客製行為。我們現在來實作一個 LoadingAjaxCallDecorator:

public class LoadingAjaxCallDecorator implements IAjaxCallDecorator {

	public CharSequence decorateOnFailureScript(CharSequence script) {
		return script + " DisabledZone.off(); ";
	}

	public CharSequence decorateOnSuccessScript(CharSequence script) {
		return script + " DisabledZone.off(); ";
	}

	public CharSequence decorateScript(CharSequence script) {
		return " DisabledZone.on(); " + script;
	}
}

這個 class 夠直覺吧!decorateScript() 是 decorate 主要的 ajax script的地方,我們在前面加一段 "DisabledZone.on();",讓網頁在執行 ajax 前,先顯示 "Loading..." 的訊息。再來,另外兩個 decorateOnFailureScript() 和 decorateOnSuccessScript() 則是 ajax 執行完之後,依失敗或成功的狀態而呼叫的 callback。在這裡,我們不論成功或失敗,後面都給他加個 "DisabledZone.off();" ,強迫關閉小紅標。完成這個 LoadingAjaxDecorator 後,我們可以重覆使用它,把它加在任何的 wicket ajax 元件裡,例如 AjaxLink 元件:

public abstract class LoadingAjaxLink extends AjaxLink {

	public LoadingAjaxLink(String id) {
		super(id);
	}

	public LoadingAjaxLink(String id, IModel model) {
		super(id, model);
	}

        //override,回傳我們剛寫好的 decorator
	@Override
	protected IAjaxCallDecorator getAjaxCallDecorator() {
		return new LoadingAjaxCallDecorator();
	}

}

只要換上這個 LoadingAjaxLink,嘿,馬上就有 "Loading..." 的小紅標了。這裡我們可以看出 LoadingAjaxCallDecorator 其實還有很大的改進空間,比方說可以加上 setDelay() 或是 setMessage() 之類的 method 提供更好的彈性。反正這是 Java 物件,愛加啥就加啥 :-)

從上面的實作裡我們也可以看出 Wicket 極力地在 web 開發裡套用各種物件導向的理念。像這裡的 AjaxCallDecorator,就採用了 decorator pattern,看了之後真是很佩服 Wicket 的設計者,虧他想的出用 decorator 來提供 ajax 的客製,真是太漂亮了。Wicket 的其他地方也是,比方說他們用了很多 Visitor pattern 來解決 component tree 的 tranversal。相較於傳統的 Action-based web framework (struts, webworks...etc),只能讓你用 procedure 式來思考,真的是白費了 Java 語言的能力。還是那句老話:Wicket 真的把物件導向帶回 Web 開發了。

ps. 有關上面的 javascript,裡面有一段 function repairIEFixedPosition(el),這個是用來修正 IE 的 CSS 不能使用 position:fixed 的老問題,上面的實作裡我呼叫另一個工具來幫忙修正,請參考 fixed.js 的網站。

ps2. 這個 javascript 在顯示 message 時,會 block 整個網頁,即使用者不能再點選 link 或是其他按鈕 (所以才叫作 DisabledZone )。這種作法有好有壞,好處是它讓使用者不會 double click 送出兩次,而且避免使用者又去按別的 link/按鈕,跑到別的網頁,反而看不到第一次按的回應。但是壞處是,這個已經不算是 (A)jax 了。Ajax 的 A 就是非同步。如果每一次做 ajax 就 block 畫面,直到 loading 結束後才釋放,這個跟一般的網頁操作的 "同步性" 沒啥兩樣。所以要依各個操作的特性,採用不同的做法,才能提供使用者安全又快適的使用經驗。

ps3. DisabledZone.js 暫時性 block 網頁的功能,只能在 firefox/safari 等 browser生效。愚蠢老舊的死 IE 並不支援... 我還在找 IE 該怎麼寫...


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>