上一篇是 ASP.NET Dynamic Data 的入門練習,主要是體驗一下 Dynamic Data 技術的威力,看看它如何幫我們在幾分鐘內完成一個資料驅動 Web 應用程式的骨幹(scaffolding)。本文則打算整理一些 Dynamic Data 技術的基本觀念,並延續上一篇的練習,提供一些客製化網頁的技巧,包括:自訂欄位(示範將欄位的顯示名稱改成中文)、使用特定的控制項來顯示/編輯欄位值 (RenderHint)。

前言

要先說明的是,這些都是我在學習 Dynamic Data 時蒐集的一些片段知識的整理,它們大部分來自以下部落格:

由於這技術還很新,我也正在摸索,若有錯誤的地方,還請各方前輩不吝指正。文章發布之後,內容也可能隨時增訂或刪減,以盡量保持完整、正確。

Dynamic Data 基礎觀念

引自 Scott Gu:「簡言之,它可以讓你迅速建立以 LINQ to SQL (未來可支援 LINQ to Entities)物件模型為基礎的資料驅動網站,而且不需要手工打造每一個網頁。」(In a nutshell this enables you to really quickly build data driven web-sites that work against a LINQ to SQL (and in the future LINQ to Entities) object model - and optionally allows you to do this without having to build any pages manually.)其中的 LINQ to SQL 僅適用於 SQL Server,而未來支援的 LINQ to Entities,則可將支援範圍擴大到其他資料庫(例如 Oracle)。

網站骨幹(scaffolding)的相關功能:

  • 迅速建立資料驅動的網站應用程式骨幹
    • 整個網站採用 ASP.NET 主版頁面(master pages)與樣板套用的方式建立,只要更改某個全域的樣板/樣式,即可套用至整個網站,無須針對個別網頁逐一修改。
    • 同時提供 grid 在地編輯(inline editing)與獨立的欄位編輯區塊,開發人員可任選一種編輯方式,或兩者皆提供。
  • 採用與 ASP.NET MVC 相同的繞徑(routing)引擎。 (這點有待確認,畢竟目前還在 preview 階段
  • 任何表格與操作功能都可以透過複製樣板再修改的方式改寫全域樣板的行為。
    • 開發人員可以將全域樣板的 ASP.NET 網頁複製一份,然後修改新的複本,以達成客製化的需求。 (這是我目前感興趣的部分)

其他功能:

  • 根據資料庫欄位形態自動選擇適當的顯示/編輯控制項,例如:字串欄位以 text box 顯示、布林欄位是 check box、以 foreign key 關聯的代碼欄位會以 combo box 顯示。如果是 foreign key 欄位,欄位值會顯示成關聯的資料表連結。
  • 可自訂欄位的顯示/編輯控制項。 
  • 可藉由修改資料模型的 meta data 來控制欄位的顯示格式(RenderHint 特徵項,DisplayFormat 和 DisplayColumn 屬性)與驗證規則(Required、Regex、Range 特徵項)。
  • 提供可分頁與排序的 DynamicGridView 控制項。
  • DynamicGridView、DynamicFormView、DynamicListView、以及 DynamicDetailsView 可用於個別網頁。

Dynamic Data 提供的快速生成網站骨幹(scaffolding)的能力,對於建立像後台管理這類的網頁應該有不少幫助(當然,一些客製化的處理是少不了的),如果不使 用網站骨幹的功能,你還是可以在個別的網頁中使用 Dynamic Data 控制項及其他功能。

客製化:自訂欄位

這裡要解決上一篇文章所遺留的問題:如何將欄位名稱改成中文。延續上一篇的範例程式,我們要修改客戶資料表的 CRUD(增刪改查)網頁,將欄位名稱改成中文。以下是練習步驟(您也可以直接下載本文的附件以取得完整原始碼):

  1. 開啟上一次練習的範例網站,在網站根目錄建立一個資料夾,名稱為 "Customers"。我們準備將全域樣板複製到這個資料夾底下,並且動手修改成自己希望的樣子。請注意,此資料夾名稱有特定規則,如果你的資料夾名稱 後面少了 "s",後續的客製化動作會完全無作用。其命名的規則與瀏覽時的 URI 樣式有關,這點稍後會補充說明。
  2. 在 Solution Explorer 中,將 App_Shared\DynamicDataPages\ 底下的 ListDetailsTemplate.aspx 複製到新建立的 Customer 資料夾(至於為什麼只修改這個檔案,請參考後面的補充說明),然後將 Customer 資料夾底下的樣板檔案改名為 "ListDetails.aspx",雙擊此檔案以進行編輯。
  3. 在編輯視窗中,切到 ListDetails.aspx 的 Design view(或 Split view),點選畫面中的 DynamicGridView 控制項,再到屬性視窗中把 AutoGenerateColumns 屬性改為 False。然後開啟 Columns 屬性的編輯器,加入一些 BoundField 欄位,參考下圖。對於熟悉 ASP.NET 2.0 的人來說,這些動作應該都不陌生。
  4. 在瀏覽器中開啟 Default.aspx,再點 Customers 連結,即可看到如下圖的執行結果:


    註:網頁上的部份中文標題只是修改一些靜態的 HTML 字串,因此略過這些步驟不寫。
     
  5. 試著修改第一筆記錄,將 ALFKI 這筆記錄的客戶編號清空,你會發現在上次練習過程中加入的欄位驗證功能失效了。這是因為我們使用 BoundField 的緣故,正確的作法應該是用 DynamicField。不過,VS2008 的欄位編輯器裡面並沒有提供 DynamicField(如步驟 3 的截圖),因此我們得直接修改 Customers\ListDetails.aspx 的網頁原始碼。做法是找到以下標籤:

            <Columns>
              <asp:BoundField DataField="CustomerID" HeaderText="客戶編號" />
              <asp:BoundField DataField="CompanyName" HeaderText="公司名稱" />
              <asp:BoundField DataField="ContactName" HeaderText="聯絡人" />
            </Columns>

    然後將裡面的 "BoundField" 改為 "DynamicField" 就行了。修改後的執行結果見下圖:

補充說明:

  • 步驟 1 建立的 Customers 資料夾,其命名規則與網頁瀏覽時的 URI 樣式有關,我們可以從 web.config 的 dynamicData 區段看出端倪:

    <dynamicData dataContextType="NorthwindDataContext" enableTemplates="true">
        <mappings queryStringKeyPrefix="" pattern="~/{table}/{viewName}.aspx">
            <add actions="list,details" viewName="ListDetails" templateFile="ListDetailsTemplate.aspx"/>
        </mappings>
    </dynamicData>

    其 中的 <mappings> 子區段即包含了 URI 對應的相關設定。從 pattern 屬性可以看出,URI 樣式是從網站根目錄之下再接資料表名稱({table}),然後是檢視頁面的名稱({viewName}.aspx),這也是為什麼步驟 1 的客戶資料表維護頁面要存放在 Customers 資料夾之下的緣故。

    此外,還有一個值得注意的設定,就是 <dynamicData> 元素的 dataContextType 屬性,它是用來指定整個 Dynamic Data 網站要使用的 data context 類別名稱,簡單地說,就是指定網站要維護的資料表來自哪個類別。此範例中的 dataContextType 屬性值為 "NorthwindDataContext",也就是建立 Northwind.dbml 時產生的對應類別。在目前的 ASP.NET 3.5 Extensions preview 版本,整個網站同時只能使用一個 data context 類別,換句話說,你必須把要維護的資料表全部拉進同一個 .dbml 裡面。未來的版本可能會解除這個限制。
     
  • App_Shared\DynamicDataPages\ 底下有三個樣板檔:DetailsTemplate.aspx、ListDetailsTemplate.aspx、以及 ListTemplate.aspx。從檔名可以很容易辨別,"Details" 樣板表示為單筆記錄的欄位明細區塊,"List" 樣板表示以 grid 顯示的記錄清單,"List" 加 "Detail" 的,當然就是兩者皆有了。
  • 執行範例時,如果瀏覽其他資料表維護網頁,例如 Orders、Categories 等,由於它們沒有對應的資料夾與和自訂網頁,因此會以預設的樣板網頁呈現。也就是說,只要有建立正確的資料夾和自訂網頁,ASP.NET Dynamic Data 繞徑(routing)引擎就會使用你的自訂網頁,否則會使用預設的樣板網頁。

客製化:使用自訂的欄位顯示/編輯控制項

預設情況下,ASP.NET Dynamic Data 網站骨幹所產生的網頁,在顯示欄位資料時,會自動根據欄位型態採用適當的控制項,例如:字串欄位會使用 text box、布林欄位會使用 check box 等等。這些欄位的顯示/編輯控制項都是 user controls,你可以在 Solution Explorer 視窗中的 App_Shared\DynamicDataFields 裡面找到這些控制項,如下圖所示:

注意其中紅色框框圈住的兩個控制項:CalendarDate.ascx 和 CalendarDate_Edit.ascx,只有這兩個控制項是我自己加進去的,其他則是 ASP.NET Dynamic Data 骨幹預先產生。這兩個控制項一個是用來顯示,一個是用來編輯日期欄位,而且編輯時會彈出日曆面板讓使用者挑選。由於預設的日期欄位編輯控制項  DateTime_Edit.ascx 只提供一般的文字方塊輸入方式,因此這裡要示範將某個日期欄位的編輯控制項改用自訂的 CalendarDate_Edit.ascx 控制項。

先不談如何設計 dynamic data fields(動態資料欄位)的控制項,假設你已經有 CalendarDate.ascx 和 CalendarDate_Edit.ascx,而且希望 Orders 資料表的 OrderDate 欄位在顯示和編輯時分別使用這兩個控制項,所需之步驟如下:

  1. 在 App_Code 資料夾中 new 一個類別: Order.cs,並依照上一篇的練習步驟修改 Order 類別,程式碼如下:

    using System;
    using System.ComponentModel;
    using System.Data;
    using System.Configuration;
    using System.Linq;
    using System.Data.Linq.Mapping;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.HtmlControls;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Xml.Linq;
    using System.Web.DynamicData;
    using System.Reflection;

    [RenderHint("OrderDate", "CalendarDate")]
    public partial class Order
    {
    }

    注意粗體字那行,該處使用 RenderHint 特徵項來指定 OrderDate 欄位要改用 CalendarDate 控制項來顯示欄位值。
  2. 這樣就行了。接著在瀏覽器中檢視網頁,進入 Orders 資料表的維護頁面,然後在 grid 中點「編輯」,當游標當移入 OrderDate 欄位時,你會看到文字方塊下方出現日曆面板,其他未特別指定的日期欄位(如 RequiredDate, ShippedDate)則維持預設的行為。參考下圖:


     

如何撰寫 Dynamic Data Fields(欄位的顯示/編輯控制項)?

這個部份就無法在此細說了,主要的原因是我也還不夠了解。此範例中的 CalendarDate.ascx 和 CalendarDate_Edit.ascx,是我參考 DateTime.ascx 和 DateTime_Edit.ascx 的原始碼再稍加修改而成,裡面用到了 AJAX Control Toolkit 的 CalendarExtender 控制項。我並未對這兩個控制項做詳細的測試,但基本上可以 work 就是了,有興趣的朋友可下載原始碼回去研究看看或嘗試改良。這裡僅列出幾個要點,方便日後參考:

  • 欲建立 Dynamic Data Field,可以在 Solution Explorer 視窗中展開 App_Shared 資料夾,在 DynamicDataFields 資料夾上點右鍵,選 Add New Item,然後選擇 "Dynamic Data Field"。此方式會幫你產生兩個 user controls,一個用來顯示欄位值,一個用來編輯欄位值,而且它們都是 single file 的寫法,而非 code-behind 的寫法。
  • 你也可以透過新增 user control 的方式來建立 Dynamic Data Field 控制項,但是要記得要將 code-behind 類別的父類別改為 System.Web.DynamicData.FieldTemplateUserControlBase,同時注意遵守以下命名規則:如果用來顯示欄 位值的控制項名稱為 Foo.ascx,那麼用做編輯的控制項就必須命名為 Foo_Edit.ascx。如未依照此命名慣例,則編輯欄位時將不會出現你設計的控制項。
  • 最後一點跟動態資料欄位無關,而是 CalendarExtender 的問題:當它出現在 UpdatePanel 包住的 GridView 裡面時,位置會跑掉,也就是不會出現在相連的 TextBox 下方。我參考了這篇討論串 http://forums.asp.net/p/1078690/1590343.aspx,其中 Denis 網友提出的解法可解決此問題。

小結

客製化 Dynamic Data 網頁的部份就先寫到這裡,基本上,這些技巧都還在 David Ebbo 的 ASP.NET Dynamic Data 教學影片範圍內,並沒有太多新東西。不過,有個問題值得提出來,就是如果有 20 個資料表維護網頁的欄位名稱都要改成中文,那不就要複製及修改樣板 20 次?如果能夠在一個集中的地方一次指定欄位的顯示名稱,應該是最好的方法。Marcin Malow 有一篇文章:Adding custom metadata providers in ASP.NET 3.5 Extensions Preview,或許跟這裡要解決的問題有關,以後再找時間了解看看吧。

 下載本文的範例原始碼:DynamicDataDemo1.zip

後記 (2008-1-25)

我到 ASP.NET 論壇提出有關欄位顯示名稱的問題,微軟 ASP.NET 開發小組成員 Marcin Dobosz 的回答是:

In the ASP.NET 3.5 Extensions Preview you can only achieve this using the 2 approaches you enumerated. In future versions you will be able to declare a DisplayNameAttribute on both tables/entities as well as properties/columns to provide custom display text.

也就是說,

目前可用的方法,就是我分別在第一篇及本文中提到的兩個方法,而 ASP.NET 3.5 Extensions 未來的版本會提供資料表及欄位物件的 DisplayNameAttribute,這會是更好的解法。

修訂歷程:

  • 2008-01-20 : 初次發布。
  • 2008-01-21 : 更新範例原始碼。
  • 2008-01-22 : 補充說明 web.config 中的網頁 URI 樣式設定。
  • 2008-01-23 : 增加 RenderHint 自訂欄位編輯控制項的用法說明,並更新範例原始碼(增加 CalendarDate 和 CalendarDate_Edit 使用者控制項)。
  • 2008-01-24 : 增加 <dynamicData> 的 dataContextType 元素的補充說明。
  • 2008-01-25 : 增加「後記」。