[Abstract]

When using ASP.NET masterpage, the ClientID of webcontrol inside ContentPlaceHolder will get container's ClientID as prefix, like 'ctl00_ContentPlaceHolder1_TextBox1' and this become a big trouble while writing Javascript client-side code. 

Many people suggest using "document.getElementById('<% =TextBox1.ClientID%>')" to solve the problem, but I really don't like embed server-side code in ASPX file, so here's my solution, a flexible Javascript "afa_mpget()" to replace document.getElementById() when using masterpage.

猜謎時間又來了! 想看看,以下的Code有什麼地方有問題?

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" 
Inherits="_Default" MasterPageFile="~/MasterPage.master" %>
<asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder1">
    <asp:TextBox ID="TextBox1" runat="server" Text="Hello"></asp:TextBox>
    <script type="text/javascript">
    alert(document.getElementById("TextBox1").value);
    </script>
</asp:Content>

答案是Javascript alert時會找不到物件,為什麼? 在網頁上View Source,答案馬上揭曉,當網頁套用MasterPage時,TextBox1產生的<INPUT>名稱會變成"ctl00_ContentPlaceHolder1_TextBox1"... 天哪! 由Frameset走向MasterPage,簡單的事變複雜了,要為此回頭嗎?

Google一下,發現大家最常建議最的解法是改寫成
alert(document.getElementById('<% =TextBox1.ClientID%>').value);

不過,我個人實在不喜歡這種ASP時代的義大利麵式寫法,所以就自力救濟了一番。首先我寫了一個MasterPageHelper.cs:

public class MasterPageHelper
{
    public MasterPageHelper()
    {
    }
 
    public static void RegisterMPGet(MasterPage mp)
    {
        List<string> lstCph = new List<string>();
        lstCph.Add(mp.ClientID);
        searchContentPlaceHolder(mp.Page.Form, lstCph);
        StringBuilder sb = new StringBuilder();
        sb.Append(@"
<script type=""text/javascript"">
function afa_mpget(objId) {
    var inp = document.getElementById(objId);
");
        foreach (string cphId in lstCph)
        {
            sb.AppendFormat(
"   if (!inp) inp = document.getElementById(\"{0}_\" + objId);\n",
                cphId);
        }
        sb.Append("return inp;\n}\n</script>");
        Literal js = new Literal();
        js.Text = sb.ToString();
        mp.Page.Form[請看下方更新]Header.Controls.AddAt(0, js);
    }
 
    public static void searchContentPlaceHolder(Control ctrl, 
List<string> lst)
    {
        if (ctrl is ContentPlaceHolder)
            lst.Add(ctrl.ClientID);
        else if (ctrl.HasControls())
            foreach (Control c in ctrl.Controls)
                searchContentPlaceHolder(c, lst);
    }
}

在MasterPage的Page_Load事件中,呼叫MasterPageHelper.RegisterMPGet(this),就會在網頁中加入一個用元件名稱自動尋找各ConentPlaceHolder下元件的彈性函數--afa_mpget(fieldName):

<script type="text/javascript">
function afa_mpget(objId) {
    var inp = document.getElementById(objId);
   if (!inp) inp = document.getElementById("ctl00_" + objId);
   if (!inp) inp = document.getElementById("ctl00_ContentPlaceHolder1_" + objId);
   if (!inp) inp = document.getElementById("ctl00_ContentPlaceHolder2_" + objId);
return inp;
}
</script>

接著,我們用afa_mpget取代document.getElementById,就又回到以前幸福快樂的生活囉!

<asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder1">
    <asp:TextBox ID="TextBox1" runat="server" Text="Hello"></asp:TextBox>
    <script type="text/javascript">
    </script>
</asp:Content>
<asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder2">
    <asp:TextBox ID="TextBox2" runat="server" Text="World"></asp:TextBox>
    <script type="text/javascript">
    alert(afa_mpget("TextBox1").value);
    alert(afa_mpget("TextBox2").value);
    </script>
</asp:Content>

Update 2008-01-03
Page.Form.Controls.AddAt會破壞ViewState,故改成Header.Controls.Add,說明在此

Update 2008-01-19
強化版搜尋範圍擴及UserControl,說明在此

Update 2009-01-13
另有jQuery版的解決方案,說明在此


Comments

# by SNP

GOOOD POINT !

# by eric

請教一下Jeffrey , 為什麼你不喜歡 document.getElementById('<% =TextBox1.ClientID%>').value 這樣的寫法呢? 難道是有什麼地雷嗎? 已經用很多的人

# by Jeffrey

to eric, 好問題,值得寫一篇Post來說明,所以看這裡: http://blog.darkthread.net/blogs/darkthreadtw/archive/2007/12/24/why-not-inline-aspx.aspx

# by Maxi

真是個好方法 RegisterMPGet這種東西要去那裡學? 其實我一本ASP.NET的書都沒看過,書本裡會有嗎? 我都用笨方法 寫接clientID做參數的Javascript function 然後在code behind為control加入OnClick attribute 存入control.ClientId做參數 改天用黑暗大的方法試看看

# by 庫洛洛

請問大大, 這個可不可改成VB.Net版的? 用線上C#轉VB.Net http://www.developerfusion.com/tools/convert/csharp-to-vb/ 下面都不能用, Dim lstCph As New List(Of String)() Public Shared Sub searchContentPlaceHolder(ByVal ctrl As Control, ByVal lst As List(Of String)) 這要改成什麼才能用

# by Jeffrey

to 庫洛各, 轉換後的程式看起來沒啥問題, 莫非是忘了宣告Imports System.Collections.Generic?

# by 庫洛洛

謝謝 Jeffrey 真的是沒加 Imports System.Collections.Generic 真的很謝謝

# by Amanda

請問,我是巢狀式的masterpage,我在第一個masterpage加入 只建出 <script type="text/javascript"> function afa_mpget(objId) { var inp = document.getElementById(objId); if (!inp) inp = document.getElementById("ctl00_ctl00" + objId); script> 請問該注意那部份呢?

# by gary

您好: 我試了你的方法,部份程式碼如下 function CloseDialog(ModalPopupExtenderId) { ModalPopupExtenderId = afa_mpget(ModalPopupExtenderId); var modalPopupExtender =ModalPopupExtenderId; $find(modalPopupExtender).hide(); } 但不知為什麼就是afa_mpget (ModalPopupExtenderId); 取出來是null,去看了原始檔,字串串的沒有錯,但就是找不到,可以請問我是什麼地方錯了嗎,謝謝您

# by Jeffrey

to gary, 我建議你看一下網頁的HTML原始碼,確定<script type="text/javascript">function afa_mpget(objId) { ... }</script>有被加進去。接著檢視其中列出的幾條ctl00_* 加上ModalPopupExtenderId是否有吻合你想要的Element ID。

# by vincent

無法修改 Controls 集合,因為控制項包含程式碼區塊 (例如 <% ... %>)。 描述: 在執行目前 Web 要求的過程中發生未處理的例外情形。請檢閱堆疊追蹤以取得錯誤的詳細資訊,以及在程式碼中產生的位置。 例外詳細資訊: System.Web.HttpException: 無法修改 Controls 集合,因為控制項包含程式碼區塊 (例如 <% ... %>)。 原始程式錯誤: 行 17: Dim js As New Literal() 行 18: js.Text = sb.ToString() 行 19: mp.Page.Header.Controls.AddAt(0, js) 行 20: End Sub 行 21: Public Shared Sub searchContentPlaceHolder(ByVal ctrl As Control, ByVal lst As List(Of String)) 執行之後產生上面的錯誤,是我那裡弄錯了嗎

# by Jeffrey

to vincent, 是<head>...</head>中間有參雜<% ... %>的寫法嗎?

Post a comment