本文章是由機器翻譯。

ASP.NET 動態資料

5 分鐘內構建資料驅動企業網站

James E.Henry

多年以來,開發人員一直忙於完成一些枯燥的任務,那就是構建具有創建、讀取、更新和刪除 (CRUD) 功能的資料層以供 UI 使用。我至今還能回想起 10 多年前的一個專案:我需要編寫代碼,以便自動從 Rational Rose 物件模型生成業務層和資料層。當時的工作量非常大。

幾年後,Microsoft 推出了 Microsoft .NET Framework 和 DataSet(資料集)物件。利用資料集設計器,您可以將強類型化的資料集連接到 SQL Server 等後端資料庫,然後在整個應用程式中使用資料集。這最大程度地減少了直接使用 SQL 的需要,但仍需要手動創建網頁以顯示資料庫中的資料。

又過了幾年,Microsoft 推出了實體框架。這種物件關係映射 (ORM) 框架使用 LINQ 來抽象資料庫架構,並將其作為概念架構呈現給應用程式。就像資料集一樣,這種技術還能最大程度地減少直接使用 SQL 的需要。但是,仍然需要手動創建網頁以顯示資料。

現在,Microsoft 推出了 ASP.NET 動態資料,它結合了實體框架和 ASP.NET 路由,讓應用程式能夠回應實際上並不存在的 URL。利用這些功能,您在幾分鐘內就能創建可用於生產的資料驅動型網站。本文將為您介紹具體的創建過程。

入門

讓我們假設以下情況:我要為一個名為 Adventure Works 的虛構公司構建 Intranet 網站。公司能夠通過這個網站管理員工資訊。

公司的資料存儲在 Microsoft SQL Server 2008 資料庫中。(您可以從 msftdbprodsamples.codeplex.com 下載和安裝該資料庫。)

接下來,打開 Microsoft Visual Studio 2010 並創建一個新的 ASP.NET 動態資料實體網站 C# 專案。

ASP.NET 動態資料實體網站專案類型利用了 ASP.NET 路由和實體框架的優勢,可讓您快速創建資料驅動型網站。若要使用這個功能,您需要在專案中添加一個實體框架資料模型。為此,請在“網站”功能表中選擇“添加新項”。在“添加新項”對話方塊中,選擇 ADO.NET 實體資料模型。將其命名為 HumanResources.edmx。

Visual Studio 隨後會提示您將模型加至 App_Code 資料夾中。選擇“是”允許其進行此項更改。創建網站專案時,Visual Studio 會動態編譯 App_Code 資料夾中的所有代碼。至於實體資料模型,Visual Studio 會自動為數據上下文和實體分別生成局部類。在本例中,Visual Studio 將代碼放到一個名為 HumanResources.Designer.cs 的檔中。

接下來,將出現“實體資料模型嚮導”,如圖 1 所示。選擇“從資料庫生成”並按一下“下一步”。

圖 1 啟動實體資料模型嚮導

接下來,選擇一個與 Adventure Works 資料庫的連接。如果不存在任何連接,您需要創建一個新的連接。图 2 顯示了與名為 Dev\BlueVision 的 SQL Server 實例上的 AdventureWorks 的連接。

图 2 配置数据连接

在下一頁上,您可以選擇 Human Resources 架構中的所有表。架構的名稱顯示在括弧中。图 3 顯示了一些選中的表。

圖 3 在 Visual Studio 中為 Human Resources 架構選擇表

按一下“完成”後,Visual Studio 會通過您為專案選擇的表自動生成實體。图 4 顯示了 Visual Studio 通過資料庫架構生成的七個實體。Visual Studio 使用資料庫中的外鍵約束來創建各個實體之間的關係。例如,Employee 實體與 JobCandidate 實體之間建立了一對多關聯性。這表示一個員工可以成為公司內多個職位的候選人。

圖 4 Visual Studio 中通過資料庫表生成的實體

另外請注意,EmployeeDepartmentHistory 實體聯接了 Employee 和 Department 實體。如果 EmployeeDepartmentHistory 表僅包含用於聯接 Employee 和 Department 表所需的欄位,則 Visual Studio 將直接忽略 EmployeeDepartmentHistory 實體。這樣就可以直接在 Employee 和 Department 實體之間進行導航。

使用 ASP.NET 路由

利用 ASP.NET 路由,應用程式可以回應實際上並不存在的 URL。例如,兩個 URL(http://mysite/Employee 和 http://mysite/Department)可以被定向到頁面 http://mysite/Template.aspx。頁面本身隨後從該 URL 提取資訊,以確定顯示員工清單還是顯示部門清單,兩個視圖均採用了相同的顯示範本。

路由就是一個映射到 ASP.NET HTTP 處理常式的 URL 模式。處理常式負責確定如何將實際的 URL 映射到已解釋的模式。ASP.NET 動態資料使用名為 DynamicDataRouteHandler 的路由處理常式來解釋 URL 模式中的 {table} 和 {action} 預留位置。例如,處理常式可以使用以下 URL 模式:

  http://mysite/{table}/{action}.aspx

該模式隨後可用於解釋 URL http://mysite/Employee/List.aspx。

Employee 將作為 {table} 預留位置處理,而 List 則作為 {action} 預留位置處理。 處理常式隨後可以顯示一系列 Employee 實體示例。 DynamicDataRouteHandler 使用 {table} 預留位置確定要顯示的實體名稱,使用 {action} 參數確定用於顯示實體的頁面範本。

Global.asax 檔包含一個名為 RegisterRoutes 的靜態方法,當應用程式第一次啟動時會調用該方法,如下所示:

public static void RegisterRoutes(RouteCollection routes) {
  // DefaultModel.RegisterContext(typeof(YourDataContextType), 
  //   new ContextConfiguration() { ScaffoldAllTables = false });
  routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
    Constraints = new RouteValueDictionary(
    new { action = "List|Details|Edit|Insert" }),
    Model = DefaultModel
  });
}

您需要做的第一件事就是取消注釋調用 DefaultModel 實例的 RegisterContext 方法的代碼。 此方法在 ASP.NET 動態資料中註冊實體框架資料上下文。 您需要按照如下所示修改代碼行:

DefaultModel.RegisterContext(
    typeof(AdventureWorksModel.AdventureWorksEntities), 
    new ContextConfiguration() { ScaffoldAllTables = true });

第一個參數用於指定資料上下文的類型,在本例中為 AdventureWorksEntities。將 ScaffoldAllTables 屬性設置為 True 後,您就可以在網站上查看所有實體。如果將此屬性設置為 False,就需要將 ScaffoldTable(true) 特性應用到要在網站上查看的每個實體。(實際上,您應當將 ScaffoldAllTables 屬性設置為 False,以防止將整個資料庫意外暴露給最終使用者。)

RouteCollection 類的 Add 方法用於將新的 Route 實例添加到路由表中。有了 ASP.NET 動態資料,Route 實例實際上就是一個 DynamicDataRoute 實例。DynamicDataRoute 類在內部將 DynamicDataRouteHandler 用作處理常式。傳遞給 DynamicDataRoute 的構造函數的參數代表處理常式應當用於處理實際 URL 的模式。還設置了一個約束,用於將 {action} 的值限制為 List、Details、Edit 或 Insert。

理論上來說,如果您要使用 ASP.NET 動態資料,所需要做的就是這些了。如果您要生成並運行應用程式,則需要參考圖 5 中所示的頁面。

圖 5 基本的 ASP.NET 動態資料網站

支援中繼資料

您應當注意的一點是,實體的名稱與它們表示的表名稱是相同的。在代碼或資料庫中,這沒有問題,但是在 UI 中卻不行。

通過 ASP.NET 動態資料,更改顯示的實體名稱很容易,只需將 DisplayName 特性應用到代表實體的類即可。因為實體類包含在一個由 Visual Studio 自動重新生成的檔中,所以您應當在單獨的代碼檔中更改局部類的代碼。因此,請將一個名為 Metadata.cs 的新代碼檔添加到 App_Code 資料夾中。然後,將以下代碼添加到該檔中:

using System;
  using System.Web;
  using System.ComponentModel;
  using System.ComponentModel.DataAnnotations;

  namespace AdventureWorksModel {
    [DisplayName("Employee Addresses")]
    public partial class EmployeeAddress { }
  }

然後重新生成並運行該應用程式。 您會注意到 EmployeeAddresses 變成了 Employee Addresses。

同樣,您也應當將 EmployeeDepartmentHistories、EmployeePayHistories 和 JobCandidates 的名稱更改為更恰當的名稱。

接下來,按一下 Shifts 連結。 這將顯示一個員工輪班清單。 我將分別把 StartTime 和 EndTime 的名稱更改為 Start Time 和 End Time。

還有一個問題,Start Time 和 End Time 的值會同時顯示日期和時間。 而在本例中,使用者實際上只需要查看時間,因此我將把 Start Time 和 End Time 值的格式定義為僅顯示時間。 您可以通過 DataType 特性為欄位指定更具體的資料類型,例如 EmailAddress 或 Time。 將 DataType 特性應用到適當的欄位。

首先,打開 Metadata.cs 檔並添加以下類定義:

[MetadataType(typeof(ShiftMetadata))]
public partial class Shift { }
public partial class ShiftMetadata { }

請注意,MetadataType 特性被應用到了 Shift 類。 通過這個特性,您可以指定一個單獨的類,為實體的欄位包含其他中繼資料。 您無法將其他中繼資料直接應用到 Shift 類的成員,因為無法將同一個類成員添加到多個局部類中。 例如,在 HumanResources.Designer.cs 中定義的 Shift 類已經擁有一個名為 StartTime 的欄位。 因此,您無法將同一欄位添加到 Metadata.cs 中定義的 Shift 類中。 此外,您不應當手動修改 HumanResources.Designer.cs,因為該檔將由 Visual Studio 重新生成。

利用 MetadataType 特性,您可以指定一個完全不同的類,以便將中繼資料應用到欄位中。 將以下代碼添加到 ShiftMetadata 類中:

[DataType(DataType.Time)]
  [Display(Name = "Start Time")]
  public DateTime StartTime { get; set; }

DataType 特性用於指定 Time 枚舉的值,以指明 StartTime 欄位的格式應當為時間值。其他枚舉的值還包括 PhoneNumber、Password、Currency 和 EmailAddress 等等。Display 特性用於指定當欄位顯示在清單中時,應當顯示為欄位的列的文本;以及當欄位以編輯或唯讀模式顯示時,應當顯示為欄位的標籤的文本。

接下來,重新生成並運行該應用程式。图 6 顯示了按一下 Shifts 連結的結果。

圖 6 使用 MetadataType 定義修訂過的 Shifts 頁面

您可以添加類似的代碼,以更改 EndTime 欄位的外觀。

現在,讓我們來看看 JobCandidates 連結。Employee 列顯示 NationalIDNumber 欄位的值。這可能沒什麼用。儘管 Employee 資料庫表不具備員工名稱這樣的欄位,但它有一個員工登錄資訊欄位,那就是 LoginID。此欄位也許可以提供有關本網站使用者的更有用的資訊。

為此,我會再一次修改中繼資料代碼,以便所有 Employee 列都顯示 LoginID 欄位的值。打開 Metadata.cs 檔並添加以下類定義:

[DisplayColumn("LoginID")]
public partial class Employee { }

DisplayColumn 特性指定來自實體的欄位名稱,用於表示該實體的實例。图 7 顯示了帶有 LoginID 的新清單。

圖 7 利用 LoginID 來標識 Employee

邊欄:.NET Framework 4 中的特性更改

對於 Microsoft .NET Framework 4,我們建議您使用 Display 特性,而非 .NET Framework 3.5 中的 DisplayName 特性。框架中顯然仍有 DisplayName,但只要能夠使用 Display 特性,我們就不建議您使用 DisplayName。

我們建議您使用 Display 而非 DisplayName 的原因有兩個。首先,Display 特性支援當地語系化,而 DisplayName 特性則不支援。

其次,您可以通過 Display 特性控制一切。例如,您可以控制欄位文本以各種方式顯示(提示框、標題等等);欄位是否顯示為篩選器;以及欄位是否在基本框架中顯示(AutoGenerate=false 為不顯示)。

因此,儘管本文的示例中顯示的代碼是完全有效的,我們還是建議您將 DisplayName 和 ScaffoldColumn 替換為 Display 特性。在類級別,您仍然需要使用 ScaffoldTable 和 DisplayName 特性。

您最好採納這些建議,因為 Microsoft 的其他團隊支援 DataAnnotations 命名空間(WCF RIA 服務),所以這樣做可以確保代碼與他們的代碼相容。

Employee 實體的 ContactID 欄位實際上引用的是 Contact 表中的一行,該表是資料庫中 Person 架構的一部分。因為我未從 Person 架構添加任何表,所以 Visual Studio 允許直接編輯 ContactID 欄位。為了強制保證關係完整性,我將禁止對此欄位進行編輯,但是仍然允許顯示此欄位以供參考。打開 Metadata.cs 檔並應用以下 MetadataType 特性,以便修改 Employee 類:

[DisplayColumn("LoginID")]
[MetadataType(typeof(EmployeeMetadata))]
public partial class Employee { }

然後按照以下方式定義 EmployeeMetadata 類:

public partial class EmployeeMetadata {
  [Editable(false)]
  public int ContactID { get; set; }
}

Editable 特性用於指定 UI 中的欄位是否可以編輯。

接下來,我將向 EmployeePayHistory 實體添加中繼資料,將 Rate 欄位顯示為 Hourly Rate,並將該欄位的值格式化為貨幣。 將以下類定義添加到 Metadata.cs 檔中:

[MetadataType(typeof(EmployeePayHistoryMetadata))]
public partial class EmployeePayHistory { }
public partial class EmployeePayHistoryMetadata {
  [DisplayFormat(DataFormatString="{0:c}")]
  [Display(Name = "Hourly Rate")]
  public decimal Rate { get; set; }
}

自訂範本

Visual Studio 專案包含一個名為 FieldTemplates 的資料夾。 此資料夾包含一些使用者控制項,用於編輯各種資料類型的欄位。 預設情況下,ASP.NET 動態資料將欄位與使用者控制項相關聯,控制項的名稱與欄位的關聯資料類型的名稱相同。 例如,Boolean.ascx 使用者控制項包含用於顯示布林欄位的 UI,而 Boolean_Edit.ascx 使用者控制項則包含用於編輯布林欄位的 UI。

您也可以選擇將 UIHint 特性應用到欄位,以確保使用不同的使用者控制項。 我將添加一個自訂欄位範本,以顯示一個 Calendar 控制項,用於編輯 Employee 實體的 BirthDate 欄位。

在 Visual Studio 中,將一個新的動態資料欄位項添加到專案中,並將其命名為 Date.ascx。 Visual Studio 會自動將另外一個名為 Date_Edit.ascx 的檔添加到 FieldTemplates 資料夾中。 我首先使用以下標記替換 Date_Edit.ascx 頁面的內容:

<asp:Calendar ID="DateCalendar" runat="server"></asp:Calendar>

然後使用圖 8 中顯示的完整類定義修改 Date_Edit.ascx.cs 檔。

圖 8 自訂 Date_EditField 類

public partial class Date_EditField : System.Web.DynamicData.FieldTemplateUserControl {
  protected void Page_Load(object sender, EventArgs e) {
    DateCalendar.ToolTip = Column.Description;
  }

  protected override void OnDataBinding(EventArgs e) {
    base.OnDataBinding(e);

    if (Mode == DataBoundControlMode.Edit && 
        FieldValue != null) {
      DateTime date = DateTime.MinValue;
      DateTime.TryParse(FieldValue.ToString(), out date);
      DateCalendar.SelectedDate = 
        DateCalendar.VisibleDate = date;
    }
  }
    
  protected override void ExtractValues(
    IOrderedDictionary dictionary) {
    dictionary[Column.Name] = ConvertEditedValue(
      DateCalendar.SelectedDate.ToShortDateString());
  }

  public override Control DataControl {
    get {
      return DateCalendar;
    }
  }
}

我將重寫 OnDataBinding 方法,將 Calendar 控制項的 SelectedDate 和 VisibleDate 屬性設置為 FieldValue 欄位的值。 FieldValue 欄位派生自 FieldTemplateUserControl,表示將要呈現的資料欄位的值。 我還要修改 ExtractValues 重寫方法,將對 SelectedDate 屬性所做的所有更改都保存到一個“欄位名稱-值”對的字典中。 ASP.NET 動態資料將使用此字典中的值來更新底層資料來源。

接下來,我需要通知 ASP.NET 動態資料,為 BirthDate 欄位使用 Date.ascx 和 Date_Edit.ascx 欄位範本。 有兩種方法可以實現此目標。 首先,我可以按照以下方式應用 UIHint 特性:

[UIHint("Date")]
public DateTime BirthDate { get; set; }

或者,我也可以按照以下方式應用 DateType 特性:

[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }

DataType 特性將資料類型名稱映射到使用者控制項名稱,從而實現自動映射。UIHint 特性可讓您在欄位類型名稱與使用者控制項名稱不匹配時擁有更強的控制能力。图 9 顯示了編輯員工的結果。

圖 9 自訂的員工編輯表單

如果您更改選定員工的出生日期並按一下“更新”,新的資料將保存到資料庫中。

PageTemplates 資料夾包含一些頁面範本,用於為實體呈現適當的視圖。預設情況下,支援五個頁面範本:List.aspx、Edit.aspx、Details.aspx、Insert.aspx 和 ListDetails.aspx。List.aspx 頁面範本用於呈現 UI,以表格資料的形式顯示實體。Details.aspx 頁面範本用於呈現實體的唯讀視圖,而 Edit.aspx 頁面範本則用於顯示實體的可編輯視圖。Insert.aspx 頁面用於呈現帶有預設欄位值的可編輯視圖。ListDetails.aspx 頁面範本讓您查看一系列實體,並在單獨的頁面上顯示選定實體的詳細資訊。

在前文中,我提到 ASP.NET 動態資料會檢查為路由定義的 {action} 參數的值,從而自動將 URL 請求路由到適當的頁面。例如,如果 ASP.NET 動態資料確定 {action} 參數的值為 List,它將使用 List.aspx 頁面範本來顯示實體清單。您可以更改現有的頁面範本或向資料夾中添加一個新的範本。如果您添加新的範本,則必須確保將該範本添加到 Global.asax 檔的路由表中。

EntityTemplates 資料夾包含一些範本,用於以唯讀、編輯和插入模式顯示實體實例。在預設情況下,此資料夾包含三個範本:Default.ascx、Default_Edit.ascx 和 Default_Insert.ascx,它們分別以唯讀、編輯和插入模式顯示實體實例。若要創建僅適用于特定實體的範本,只需向資料夾中添加一個新的使用者控制項並將其命名為實體集的名稱即可。例如,如果您將名為 Shifts.ascx 的新使用者控制項添加到資料夾中,ASP.NET 動態資料將使用此使用者控制項為 Shift 實體(Shifts 實體集)呈現唯讀模式。同樣,Shifts_Edit.ascx 和 Shifts_Insert.ascx 分別為 Shift 實體呈現編輯和插入模式。

對於每一個實體清單,ASP.NET 動態資料使用實體的外鍵欄位、布林欄位和枚舉欄位來構建篩選器清單。篩選器作為 DropDown 控制項添加到清單頁面中,如圖 10 所示。

圖 10 在頁面中包含資料篩選器

對於布林篩選器,DropDownList 控制項只包含三個值:All、True 和 False。對於枚舉篩選器,DropDownList 控制項包含所有枚舉的值。對於外鍵篩選器,DropDownList 控制項包含所有不同的外鍵值。篩選器被定義為 Filters 資料夾中的使用者控制項。在預設情況下,只存在三種使用者控制項:Boolean.ascx、Enumeration.ascx 和 ForeignKey.ascx。

趕緊行動起來吧!

儘管這只是一種虛構的情況,您還是看到了,完全有可能在幾分鐘內就創建一個完全可以運行的 Human Resources 網站。隨後,我將添加中繼資料和自訂欄位範本以增強 UI。

ASP.NET 動態資料提供了現成的功能,讓您可以快速建立並運行一個網站。但是,它還是完全可自訂的,因此滿足不同開發人員和組織的需求。ASP.NET 動態資料支援 ASP.NET 路由,這使您可以重複利用 CRUD 操作的頁面範本。如果您不得不頻繁執行乏味的任務,以便在每個 Web 應用程式專案中實現 CRUD 頁面,那麼 ASP.NET 動態資料就可以讓您的工作倍加輕鬆。

James Henry  是 BlueVision LLC 的獨立軟體發展人員,BlueVision LLC 是一家專門提供 Microsoft 技術諮詢的公司。他出版了兩本書:《Developing Business Intelligence Solutions Using Information Bridge and Visual Studio .NET》(Blue Vision,2005)和《Developing .NET Custom Controls and Designers Using C#》(Blue Vision,2003)。您可以通過電子郵件 msdnmag@bluevisionsoftware.com 與他聯繫。

衷心感謝以下技術專家對本文的審閱: Scott Hunter