top
Loading...
為ASP.NET控件加入快捷菜單
雖然快捷菜單在桌面應用程序中已經是非常通用的元素,但是由于在象ASP.NET這樣的基于服務器的技術沒有很好的描繪,所以在Web應用程序中快捷菜單并不是很通用。要想使用快捷菜單,瀏覽器必須高度支持DHTML和豐富事件模式,例如:Microsoft Internet Explorer 5.0及更高版本,或者Netscape 6.0等。然而,各種瀏覽器的對象模式雖然功能上大至相同,但是成員各名字卻各不相同,這就需要我們自己實現對象和事件的從一種模式到到另一種模式的轉換。





這里,我們創建一個針到Internet Explorer的對象模式的ASP.NET快捷菜單。接著,我將經過修改使其功能支持其它瀏覽器。代碼支持ASP.NET 1.x,也可以在ASP.NET2.0 Beta1下編譯通過 Outline of the
ContextMenu Control 快捷菜單控件概述:

MSDN Liabrary 中包含了幾個DHTML快捷菜單的示例。分別提供了對這一功能的不能實現方法。一個快捷菜單就是在頁面中任何位置的一組標記代碼。它包括兩部分內容——界面和腳本(當用戶在頁面右擊時關聯UI),UI提供一個可點擊的元素的列表——菜單項——和各自的內容文本,圖標,命令名(command name),目標url,提示(tooltip),和所有你認為有必要顯示出來的東西。快捷菜單界面是頁面的一部分,并且屬于頁面控件樹中的一員。(太多的快捷菜單將會出現執行的問題:與大多數用戶的常規情況相比,ASP.NET將發送更多的快捷菜單到瀏覽器)當用戶在頁面的一個元素上右擊時,將引發一段JavaScript,這段腳本將快捷菜單移動到右擊的位置顯示。

快捷菜單將保持隱藏屬性,只有當用戶在頁面的一個綁定個快捷菜單的元素上右擊時,快捷菜單才顯示。頁面元素接收腳本事件并彈出一個快捷菜單作為響應。腳本事件信賴于瀏覽器,在Internet Explorer 5.0和更新版本中是 OnContexMenu事件.在Netscape6.0和更新版本中你要使用 OnMouseUp 事件(在Internet Explorer 5.0+中你也可能使用OnMouseUp事件,但要多寫幾行代碼)在接收事件的腳本中,獲取快捷菜單的UI代碼塊并且將它移動到發生點擊的位置。同時設置彈出的panel的 visibilty屬性。當用戶在菜單項上點擊時,頁面回傳并引發服務器端事件。對于服務器而言,點擊一個菜單項和點擊一具常規的按鈕沒什么區別。

當用戶戶想取消已經打開的快捷菜單時怎么辦呢?在windows程序中,按下Esc鍵或者在菜單區域外點擊都可以取消已彈出的快捷菜單。所以你必須在Web中實現這一功能。請注意,只有快捷菜單處于活動(顯示出來)時,Esc鍵才有郊。因為Esc還可用于其它元素的其它鍵盤快捷方式。

我還可以讓菜單在用戶鼠標移開菜單時隱藏。可以通過腳本操作 OnMouseLeave 事件實現。

使用彈出菜單控件

假定我們已以設計出了這個彈出菜單(我們一會兒再說如何實現這個快捷菜單控件),那么如何使用它在ASP.NET頁面中添加一個或多個快捷菜單呢?首先,在Visual Studio? .NET的工個欄中拖一個或多個快捷菜單控件到頁面上。然后,為每個菜單添加菜單項,并配置每個菜單項的的工具提示,命令名(command name)和其它所需的內容,例如快捷鍵和幫助主題的鏈接。命令名(command name)用于在響應點擊快捷菜單發生頁面回傳時確定是哪一個菜單項被點擊;對每一個快捷菜單控件實例的菜單項集合它必須是唯一的。

你必須在HTML標簽的OnContextMenu事件中加入代碼來彈出快捷菜單,代碼必須信賴一系列的參數,象點擊的x,y坐標,點擊的元素,和要使用的快捷菜單的實例等。注意:如果必要你可以使用這種方式完全代替瀏覽器的快捷菜單。綁定到OnContextMenu事件執行的JavaScript代碼是在運行時動態生成的。ContextMenu控件將暴露一個集合屬性來包含分綁定快捷菜單的控件集合,在ContextMenu控件將在運行時給這些要綁定的控件一個 oncontextmenu 屬性。OK!完成!可以測試了。

慢著,讓我們先來想一下ContexMenu控件和頁中任意的顯示快捷菜單的元素的綁定機制,這種機制對設計時的支持怎么樣?理想的情況是:根據基類每一個Web控件直接暴露一個 ContextMenuId屬性。然后在屬性窗口中選擇這個屬性時,將看到在頁面中的ContextMenu控件的列表。當然這些ContextMenu控件我們是已經創建了的;ContextMenuId屬性在ASP.NET 1.x中不支持,在將來的ASP.NET2.0中也不支持。

在Visual Studio .NET 2003 集成開發環境中,ASP.NET復合控件可以很好的完成這一工作。可以通過使用類撰寫組合現有控件來創作新控件。復合控件可呈現一個重新使用現有控件功能的用戶界面。復合控件可以從子控件的屬性合成屬性并處理由子控件引發的事件。它還可以公開自定義屬性和事件。

我不選擇使用復合控件有以下幾個原因:一個是Visual Studio .NET 2003對于Web窗體中的控件功能的擴展支持的不好,第二,在Visual Studio 2005的ASP.NET設計器中不再支持組件托盤區。Web窗體設計器現在僅支持ASP.NET控件而忽略象復合控件這樣的非可視化的組件。Visual Studio 2005將不再信賴InitializeComponent節,并且不再在代碼文件中自動添任何工具生成(tool-generated)的代碼。ASP.NET控件也不設計成具有快捷菜單,所以要綁定快捷菜單只能通過快捷菜單控件自身的執行。這里我使用類似ASP.NET驗證控件和被驗證控件之間關聯的形式。

編程接口

我們的ContextMenu控件從WebControl繼承并執行INamingContainer接口

public class ContextMenu : WebControl, INamingContainer

圖一控件的成員細節,如下:

屬性 描述
AutoHide 標志當用戶鼠標移出控件區域時,是否自動隱藏快捷菜單
BoundControls 返回使用快捷菜單的控件集合
CellPadding 返回或設置每個菜單項周圍的空間的象素數
ContextMenuItems 返回菜單項的集合
RolloverColor 返回或設置當鼠標劃過菜單項時突顯的顏色
方法 描述
GetEscReference 返回當用戶按下Esc鍵時用于隱藏頁面中的快捷菜單的JavaScrip代碼
GetMenuReference 返回一段JavaScript代碼,這段代碼將關聯到快捷菜單所對應的HTML元素上.
GetOnClickReference 返回當用戶在菜單區域外點擊時隱藏快捷菜單的代碼.
事件 描述
ItemCommand 當用戶點擊一個快捷菜單項進激發.

關鍵屬性是ContextMenuItmes集合屬性,它包含了ContextMenuItem類型的對象集合,每一個對象表示一個菜單項。ContextMenuItem類的源碼如下:

[TypeConverter(typeof(ExpandableObjectConverter))]
public class ContextMenuItem
{
public ContextMenuItem() {}
public ContextMenuItem(string text, string commandName)
{
_text = text;
_commandName = commandName;
}
private string _text;
private string _commandName;
private string _tooltip;
public string Text
{
get {return _text;}
set {_text = value;}
}
public string CommandName
{
get {return _commandName;}
set {_commandName = value;}
}
public String Tooltip
{
get {return _tooltip;}
set {_tooltip = value;}
}
}

每個菜單項具有顯示文本,命令名(command name),提示文本(tooltip)。你可以通過各方法擴展這個類,例如添加一個圖片URL,一個不可用狀態,或一個目標URL等。顯示文本被顯示于菜單項上;命令名是一個唯一標識字符串,用于指定或確定與菜單項關聯的命令。tooltip獲取或設置當鼠標指針停留在菜單項上時顯示的工具提示文本

當用戶點擊菜單項時,頁面回傳并激發一個服務器端的ItemCommand事件。控制頁通過操作這一事件來執行一些代碼來響應用戶對菜單項的點擊。圖3是一個使用快捷菜單的示例工程的截圖:



要使用快捷菜單,你需要使用menu item對象填充ContextMenuItems集合,調整一些可視化樣式,至少添加一個控件到BoundControls集合中。然后在瀏覽器中打開示例頁,在任意綁定快捷菜單的控件上右擊。效果如圖:



每個菜單項包含一個LinkButton控件,這個LinkButton控件有一個內部綁定的點擊事件處理程序。當檢測到點擊時,頁面回傳并激發這個點擊事件。接著,預定義的處理程序將事件冒泡到上一級,并改名為ItemCommand。

控件還定義了一些可視化的屬性,象CellPadding,RolloverColor和AutoHide。重申一下,在Windows中快捷菜單可以在按下Esc鍵或在菜單區域外點擊時取消。對于基于Web的快捷菜單來說,AutoHide屬性為快捷菜單的根標簽添加OnMouseLeave腳本,所以當用戶的鼠標離開菜單區域時,這個根標簽的子樹將隱藏。把AutoHide作為一個可設置的屬性,用戶可以在需要的時候設置是否在鼠標離開時自動隱藏快捷菜單。

要使菜單能夠在點擊或按Esc鍵時隱藏,需要添加如下處理程序:< br><bodyonkeypress="..."onclick="...">

處理程序腳本可以被程序化的添加到每一個頁面元素,只要這個元素被標記為runat=server。這樣實際上就是在ContextMenu快捷菜單控件和頁面之間創建了一個邏輯信賴。另外,你必須在頁面上定義一個額外的服務器控件。當然,在運行時實例化一個額外的控件并不會嚴重的影響執行效果,但是為什么僅僅因為想容易的consume其它的控件而實例化一個無用的控件呢。作為選擇下面這個方法也可以達到同樣的效果:用body獲取按Esc鍵和鼠標點擊,并且你節省了服務器控件的開支

<body onkeypress="<% = ContextMenu1.GetEscReference() %>" onclick="<% = ContextMenu1.GetOnClickReference() %>">

讓我們更詳細的說一下控件的實現

控件的實現

ContextMenu控件的核心在于重寫CreatechildControls方法。在這個方法中控件創建界面并在頁面中寫入所需的腳本。我們說過,ContexMenu控件的用戶界面分為兩部分——圖形和腳本。我們先說圖形。

CreateChildControls方法產生一個可以在頁面移動的HTML塊,它就是所需的彈出菜單。照這樣看來,快捷菜單就是包含一個table表單的<DIV> ,每一個菜單項就是這個table表單中的一行。使用table是由于一系列的開發點(象邊框和浮動層)和它能很容易的擴展(例如添加側邊圖象)所決定的。

HtmlGenericControl div = new HtmlGenericControl("div");
div.ID = "Root";
div.Style["Display"] = "none";
div.Style["position"] = "absolute";
if (AutoHide)

  div.Attributes["onmouseleave"] = "this.style.display=’none’";

我們使用層疊樣式表(CSS)來隱藏最外層的<DIV>標簽,并且用絕對位置標記這個<DIV>。如果自動隱藏可用,那么<DIV> 還得處理 mouse-leave(鼠標離開)事件來隱藏本身。那么 onmouseout和onmouseleave事件有什么不同呢?前者是當鼠標移動到一個新的元素上時發生,而后者是當鼠標移出綁定對象時發生。舉例說明:你的鼠標在一個有兩行的表單(table)上移動。當你在table的兩行之間移動時,onmouseout事件就會發生;只有你的鼠標移動到table表單之外時onmouseleave事件才發生。

Table包含和要顯示的菜單項個數相同的行,每一行有一個單元格,每個單元格里放一個LinkButton對象。菜單通過一個循環創建:

foreach(ContextMenuItem item in ContextMenuItems)
{
TableRow menuItem = new TableRow();
menuTable.Rows.Add(menuItem);
TableCell container = new TableCell();
menuItem.Cells.Add(container);
LinkButton button = new LinkButton();
container.Controls.Add(button);
...
}

行的單元格有一組腳本操作——onmouseover和onmouseout——完成鼠標劃過的效果。當鼠標劃過時改變背景顏色。鼠標離開時恢復初始顏色。默認的背景色由從WebControl繼承的background屬性指定。高亮顏色由新的屬性RolloverColor指定。

string color = String.Format(ContextMenu.OnMouseOver, ColorTranslator.ToHtml(RolloverColor));
container.Attributes["onmouseover"] = color;
color = String.Format(ContextMenu.OnMouseOut, ColorTranslator.ToHtml(BackColor));
container.Attributes["onmouseout"] = color;

你需要把.NET的System.Drawing.Color值轉換成可用的HTML顏色。有意思的是,無論是Color類的ToString方法還是它的Name屬性都不能在所有情況下返回對應的HTML顏色字符串,不知是不是有意這樣的:(。Name屬性基本可以實現這一功能,只有和種情況例外。當顏色不能和已知的顏色匹配時,這個屬性返回顏色的RGB組,外加alpha通道值。要得到當前的HTML顏色,你必須移除alpha通道值(通常是開頭的十六進制 ff 字符串)并使用#號替換它。幸運的是,System.Drawing.ColorTranslator類可以自動完成這項工作:)。

我們接著要對鏈接按鈕做一些調整,以使其正常工作。 也就是:把鏈接的寬度設成100%以確保當沒有其它設置時,在整個行上光標都是手狀。 同樣的,要設置從對應的菜單項對象得到的文本(text),工具提示(tooltip),和命令名(command name)。最后為點擊事件關聯一個處理器。

LinkButton button = new LinkButton();
container.Controls.Add(button);
button.Click += new EventHandler(ButtonClicked);
button.Width = Unit.Percentage(100);
button.ToolTip = item.Tooltip;
button.Text = item.Text;
button.CommandName = item.CommandName;

回傳時,事件源被識別為在ContextMenu命名容器內的LinkButton,并且得到處理回傳事件的時機。點擊事件的內部處理器將所有信息打包進一個新的ItemCommand 事件并引激活這個事件,參見下面的代碼

private void ButtonClicked(object sender, EventArgs e)
{
LinkButton button = sender as LinkButton;
if (button != null)
{
CommandEventArgs args = new CommandEventArgs( button.CommandName, button.CommandArgument);
OnItemCommand(args);
}
}
protected virtual void OnItemCommand(CommandEventArgs e)
{
if (ItemCommand != null)
ItemCommand(this, e);
}

頁面上的代碼將獲得兩部分內容:引發事件的ContextMenu實例;與點擊項相關的command name。

這時,table就是快捷菜單的用戶界面。它一開始被放置在頁面的任意位置,并且使用CSS樣式表在視圖中隱藏。在用戶右擊時,這段HTML代碼塊(使用絕對位置方式定位)將被顯示成一個快捷菜單。JavaScript代碼負責截取事件并把菜單移動動所需位置,參見如下代碼:

<script language="Javascript">
function __showContextMenu(menu)
{
var menuOffset = 2 menu.style.left = window.event.x - menuOffset;
menu.style.top = window.event.y - menuOffset;
menu.style.display = "";
window.event.cancelBubble = true; return false;
}
function __trapESC(menu)
{
var key = window.event.keyCode;
if (key == 27)
{
menu.style.display = ’none’;
}
}
</script>

__showContextMenu函數設置快捷菜單對象的Top和Left屬性,以使它在點擊發生的位置顯示。少量的偏移是確保當快捷菜單顯示時,鼠標已經處于它的上面。這樣也防止由于鼠標在菜單邊緣的輕微移動而使菜單自動隱藏。鼠標事件的冒泡也必須被停止,這樣在文檔對象模型層次中高層的結點就不會捕獲右擊事件。

那么由誰來調用__showContextMenu函數呢?答案是:瀏覽器。當瀏覽檢測到有和HTML元素的onContextMenu事件相關聯的函數時,它會調用這個函數。我們以前說過OnContextMenu事件是Internet Explorer特有的事件,Netscape瀏覽器不支持此事件。作為代替可以使用onmouseup事件來檢測和處理松開鼠標右鍵的事件。

快捷菜單也負責為每個控件或為注冊快捷菜單的頁面元素添加oncontextmenu事件處理器。我定義了兩種方式使一個元素獲取它的快捷菜單.BoundControls集合屬性是一個數組。它由需要使用該快捷菜單的所有頁面控件填充,并且完成填充的代碼寫在頁面代碼中.如下:

void Page_Load(object sender, EventArgs e)
{
ContextMenu1.BoundControls.Add(Button1);
...
}

以上代碼產生如下標記:

<input type=submit ... oncontextmenu="__showContextMenu(...)" />

當用戶在控件上右擊時會彈出快捷菜單。這種方法要求任何使用自定義快捷菜單的元素必須是服務器控件,有時這種要求是不合適的。例如:假使你想為一個圖片使用自定義的快捷菜單。你必須把圖片標記<img>寫成 runat=server。其實沒必要這樣。看下面的例子:

<img oncontextmenu="<% = ContextMenu1.GetMenuReference() %>" src="...">

GetMenuReference方法返回一段用于調出快捷菜單的腳本。這樣頁面元素不需要定義成服務器控件也能具有所需的快捷菜單

標記絕對位置

這里開發的快捷菜單控件需要絕對位置功能。而這一功能并不是所有的瀏覽器都支持的。但是,一個支持復雜對象模式和豐富事件的瀏覽器應該具有高級的定位能力。

就我們關心的Internet Exploer而言,還有其它方式實現快捷菜單。我們不再使用在頁面中移動Div的位置這一方法,我們可以創建一個彈出窗口并在指定的位置顯示它。然后將DIV動態加載到彈出窗口中呈現為快捷菜單

當實現快捷菜單控件的時候,我首先選擇了上述的創建并顯示一個彈出窗口。我發現彈出窗口的一些好處,其中之一就是彈出窗口對象不需寫代碼就自動具有類似桌面快捷菜單的功能:在區域外單擊或點擊Esc鍵時自動隱藏。

但是,在Internet Exploer6.0的查看代碼功能中出現問題。按照我的測試,在彈出對像中包含快捷菜單改變了頁面的元素樹并影響查看源代碼窗口,不能顯示。同樣,我想讓快捷菜單像交互ASP.NET控件一樣可以提交當前頁(回傳視圖狀態和輸入域)而不只是跳轉到指定的URL。從內部彈出窗口提交到父窗口仍然需要更改頁面級別的棘手的代碼。(這在ASP.NET 2.0中利用跨頁提交功能會更簡單)。在MSDN的示例中,由于它僅僅時鏈接到一個外部URL所以它游刃有余。

ASP.NET回傳是一個小技巧,在當前頁面中使用絕對位置來維持快捷菜單不會改變頁面的元素結點樹。這樣就沒有上述的問題了。對于典型解除彈出菜單的事件捕獲,你仍可使用鼠標事件捕獲。這可能通過使用一系列的Internet Exploer DHTML方法來實現。

綜述

快捷菜單最重要的部分是項目項列表,它即可以在運行時通過代碼來指定,也可以在設計時指定。下面的代碼塊是設計時指定的例子:

<cc1:contextmenu id="ContextMenu1" runat="server">
<cc1:ContextMenuItem Text="做這個" CommandName="ThisCommand" Tooltip="?­" />
<cc1:ContextMenuItem Text="做那個" CommandName="ThatCommand" Tooltip="?­" />
<cc1:ContextMenuItem />
<cc1:ContextMenuItem Text="思考 ..." CommandName="ThinkCommand" Tooltip="?­ " />
</cc1:contextmenu>

空的<cc1:Contextmenu>標簽表示一個菜單項分隔。注意,我們可以通過一系列的元數據屬性設置,達到在Visual Studio.Net中操作子標簽的目的:

[DesignerSerializationVisibility( DesignerSerializationVisibility.Content)] [PersistenceMode(PersistenceMode.InnerDefaultProperty)] public ContextMenuItemCollection ContextMenuItems {...}

但是這種配置不支持其它類型的子標簽,如果你使用其它類型的子標簽,解析器將會發出一個錯誤。例如:你不能在快捷菜單的根標簽內序列化數據綁定控件的內容。通過使用一組不同的設計時屬性,可以達到這一目的(我希望在將來可以包含這方面的控件設計內容)

如果在Visual Studio.Net的設計器中雙擊快捷菜單控件,一個關聯到快捷菜單ItemCommand事件的處理器將被自動添加。然后你就可以根據name分別處理菜單項的行為。事件處理代碼中填寫的內容大至如下所示:

void ContextMenu1_ItemCommand(object sender, CommandEventArgs e) {
switch(e.CommandName)
{
case "ThinkCommand":
...
break;
case "ThisCommand":
...
break;
default:
...
break;
}
}

在圖3中你可以看到快捷菜單控件在設計時的呈現。ASP.NET設計器調用被設計在頁面內的所有控件的RenderControl方法。那么就我們的快捷菜單控件而言,將通過呈現一個table行來模擬一個選擇的菜單項,這又是如何通過RenderControl方法實現的呢?這是通過快捷菜單控件的自定義設計器來實現的。你可以在本文所附的代碼中找到這個組件的源代碼。概括的說:自定義設計器得到由RenderControl方法產生的HTML字符串,并修改這個HTML字符串,來額外添加一個不同背景色的Table行。通過這種方式用戶可以清楚的看到控件的輸出效果。

ASP.NET 2.0 中的菜單

本文和我們的快捷菜單控件都是基于 ASP.NEt 1.x 的,但是它也可以很容易的被用于 ASP.NET 2.0。也許你知道,在ASP.NET 2.0中有了全新的菜單控件。但是,你沒必要在ASP.NET 2.0應用程序中使用這個菜單控件作為快捷菜單,因為它過于龐大,并且是被設計用于其它場景的。ASP.NET 2.0 的菜單控件是專門設計成靜態菜單(而不是快捷菜單)的,并且它缺少實現快捷菜單功能所必須的兩個關鍵設置:它不能被隱藏并且不支持絕對位置。這兩個設置都可以通過編輯標記來添加聲明。在另一方面,ASP.NET 2.0 的菜單提供了一些關鍵改進,包括:支持多級嵌套,可訪問,和改變瀏覽器窗口大小時的滾動條。由你決定哪種形式更適合你的需求。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗