天天看點

[轉] ASP.NET MVC 模型綁定的功能和問題

摘要:本文将與你深入探究 ASP.NET MVC 模型綁定子系統的核心部分,展示模型綁定架構的每一層并提供擴充模型綁定邏輯以滿足應用程式需求的各種方法。 同時,你還會看到一些經常被忽視的模型綁定技術,并了解如何避免一些最常見的模型綁定錯誤。

  ASP.NET MVC 模型綁定通過引入自動填充控制器操作參數的抽象層、處理通常與使用 ASP.NET 請求資料有關的普通屬性映射和類型轉換代碼來簡化控制器操作。 雖然模型綁定看起來很簡單,但實際上是一個相對較複雜的架構,由許多共同建立和填充控制器操作所需對象的部件組成。

  本文将與你深入探究 ASP.NET MVC 模型綁定子系統的核心部分,展示模型綁定架構的每一層并提供擴充模型綁定邏輯以滿足應用程式需求的各種方法。 同時,你還會看到一些經常被忽視的模型綁定技術,并了解如何避免一些最常見的模型綁定錯誤。

  模型綁定基礎知識

  為了了解什麼是模型綁定,讓我們首先看看從 ASP.NET 應用程式請求值填充對象的傳統方法,如圖 1 所示。

  圖 1 從請求直接檢索值

public ActionResult Create()
    {
      var product = new Product() {
        AvailabilityDate = DateTime.Parse(Request["availabilityDate"]),
        CategoryId = Int32.Parse(Request["categoryId"]),
        Description = Request["description"],
        Kind = (ProductKind)Enum.Parse(typeof(ProductKind), 
                                       Request["kind"]),
        Name = Request["name"],
        UnitPrice = Decimal.Parse(Request["unitPrice"]),
        UnitsInStock = Int32.Parse(Request["unitsInStock"]),
     };
     // ...
              }
                  

  然後将圖 1 和圖 2 中的操作進行對比,圖 2 利用模型綁定生成相同的結果。

  圖 2 原始值的模型綁定

public ActionResult Create(
      DateTime availabilityDate, int categoryId,
        string description, ProductKind kind, string name,
        decimal unitPrice, int unitsInStock
      )
    {
      var product = new Product() {
        AvailabilityDate = availabilityDate,
        CategoryId = categoryId,
        Description = description,
        Kind = kind,
        Name = name,
        UnitPrice = unitPrice,
        UnitsInStock = unitsInStock,
     };
     
     // ...
              }
                  

  盡管兩個示例都實作了相同目的(即填充 Product 執行個體),圖 2 中的代碼依靠 ASP.NET MVC 将請求中的值轉換為強類型化的值。 使用模型綁定,控制器操作可以專注于提供業務值,并避免在普通請求映射和解析上浪費時間。

  複雜對象的綁定

  即使是簡單、原始類型的模型綁定也可以産生深遠影響,但許多控制器操作都不僅僅依靠幾個參數。 幸運的是,ASP.NET MVC 可以處理原始類型和複雜類型。

  下面的代碼在 Create 操作中多執行了一次傳遞,跳過原始值并直接綁定到 Product 類:

public ActionResult Create(Product product)
    {
      // ...
              }
                  

  同樣,該代碼與圖 1 和圖 2 中的操作生成相同的結果,但這次根本沒有調用代碼,複雜的 ASP.NET MVC 模型綁定消除了建立和填充新 Product 執行個體所需的全部樣闆代碼。 該代碼說明了模型綁定的實際強大功能。

  分解模型綁定

  既然你已經了解操作中的模型綁定,現在是時候分解構成模型綁定架構的元件了。

  模型綁定可以分解為兩個不同步驟: 從請求收集值并使用這些值填充模型。 這些步驟分别由值提供程式和模型綁定程式來完成。

  值提供程式

  ASP.NET MVC 包括值提供程式的實作,這些實作涵蓋了大多數常見請求值源,例如查詢字元串參數、表單字段和路由資料。 在運作時,ASP.NET MVC 使用 ValueProviderFactories 類中注冊的值提供程式計算模型綁定程式可以使用的請求值。

  預設情況下,值提供程式集合按下面的順序計算來自各種源的值:

  1. 以前綁定的操作參數(當該操作為子操作時)
  2. 表單字段 (Request.Form)
  3. JSON 請求主體中的屬性值 (Request.InputStream),但僅當該請求為 AJAX 請求時
  4. 路由資料 (RouteData.Values)
  5. 查詢字元串參數 (Request.QueryString)
  6. 已釋出檔案 (Request.Files)

  值提供程式集合如同 Request 對象一樣,實際上隻不過是一個所謂的字典,即模型綁定程式可以使用且無需知道資料來源的鍵/值對的抽象層。 然而同 Request 字典相比,值提供程式架構進一步實作了這種抽象,它允許你完全控制模型綁定架構擷取其資料的方式及位置。 你甚至可以建立自己的自定義值提供程式。

  自定義值提供程式

  建立自定義值提供程式的最低要求非常簡單: 建立實作 System.Web.Mvc.ValueProviderFactory 接口的新類。

  例如,圖 3 示範了從使用者的 cookie 中檢索值的自定義值提供程式。

  圖 3 檢查 Cookie 值的自定義值提供程式工廠

public class CookieValueProviderFactory : ValueProviderFactory
    {
      public override IValueProvider GetValueProvider
      (
        ControllerContext controllerContext
      )
      {
        var cookies = controllerContext.HttpContext.Request.Cookies;
     
        var cookieValues = new NameValueCollection();
        foreach (var key in cookies.AllKeys)
        {
          cookieValues.Add(key, cookies[key].Value);
        }
     
        return new NameValueCollectionValueProvider(
          cookieValues, CultureInfo.CurrentCulture);
      }
    }
                  

  請注意 CookieValueProviderFactory 非常簡單。 CookieValueProviderFactory 簡單地檢索使用者的 cookie 并利用 NameValueCollectionValueProvider 将這些值向模型綁定架構公開,而不是從頭建構一個全新的值提供程式。

  在建立自定義值提供程式之後,你需要通過 ValueProviderFactories.Factories 集合将其添加到值提供程式的清單中:

var factory = new CookieValueProviderFactory();
    ValueProviderFactories.Factories.Add(factory);
                  

  建立自定義值提供程式非常簡單,但執行此操作時請務必小心。 ASP.NET MVC 提供的現有值提供程式集能夠很好地公開 HttpRequest 中大多數可用資料(可能 cookie 除外),并且通常提供足夠的資料以滿足大多數情況的需要。

  要确定建立值提供程式對于特定情況來說是否是正确的,請回答以下問題: 現有值提供程式提供的資訊集是否包括我需要的所有資料(但可能采用錯誤格式)?

  如果不包括,添加自定義值提供程式可能是彌補缺少部分的正确方法。 但是,如果包括(通常情況),請考慮如何通過自定義模型綁定行為通路值提供程式提供的資料來彌補缺少部分。 本文的其餘部分介紹如何執行此操作。

  負責使用值提供程式提供的值建立和填充模型的 ASP.NET MVC 模型綁定架構主要元件被稱為模型綁定程式。

  預設模型綁定程式

  ASP.NET MVC 架構包括名為 DefaultModelBinder 的預設模型綁定程式實作,其旨在高效綁定大多數模型類型。 它通過對目标模型的各個屬性使用相對較簡單的遞歸邏輯來實作該目的:

  1. 檢查值提供程式,以便通過檢視屬性名稱是否注冊為字首來确定該屬性是作為簡單類型還是複雜類型發現。 字首僅僅是 HTML 表單字段名“點表示法”,用于表示值是否是複雜對象的屬性。 字首模式為 [ParentProperty].[Property]。 例如,名稱為 UnitPrice.Amount 的表單字段包含 UnitPrice 屬性的 Amount 字段的值。
  2. 從屬性名稱的注冊值提供程式擷取 ValueProviderResult。
  3. 如果值為簡單類型,請嘗試将其轉換為目标類型。 預設的轉換邏輯利用屬性的 TypeConverter 從字元串類型的源值轉換為目标類型。
  4. 但如果屬性為複雜類型,則執行遞歸綁定。

  遞歸模型綁定

  遞歸模型綁定高效地重複啟動整個模型綁定過程,但使用目标屬性的名稱作為新字首。 使用此方法,DefaultModelBinder 能夠周遊整個複雜對象圖表,甚至填充深度嵌套的屬性值。

  要在操作中檢視遞歸綁定,請将 Product.UnitPrice 從簡單的小數類型更改為自定義類型 Currency。 圖 4 顯示兩個類。

  圖 4 帶複雜 Unitprice 屬性的 Product 類

public class Product
    {
      public DateTime AvailabilityDate { get; set; }
      public int CategoryId { get; set; }
      public string Description { get; set; }
      public ProductKind Kind { get; set; }
      public string Name { get; set; }
      public Currency UnitPrice { get; set; }
      public int UnitsInStock { get; set; }
    }
     
    public class Currency
    {
      public float Amount { get; set; }
      public string Code { get; set; }
    }
                  

  執行此更新時,模型綁定程式将查找名為 UnitPrice.Amount 和 UnitPrice.Code 的值以便填充複雜的 Product.UnitPrice 屬性。

  DefaultModelBinder 遞歸綁定邏輯甚至可以高效地填充最複雜的對象圖表。 到目前為止,你見過了位于對象層次結構的一個層級深度的複雜對象,DefaultModelBinder 可以輕松處理此對象。 要示範遞歸模型綁定的實際強大功能,請将名為 Child 的新屬性添加到相同類型的 Product:

public class Product {
      public Product Child { get; set; }
      // ...
              }
                  

  然後,将新字段添加到表單(應用點表示法訓示每一層),根據所需層數建立層。 例如:

<input type="text" name="Child.Child.Child.Child.Child.Child.Name"/>
                  

  該表單字段将生成包含六個層的 Product! 對于每個層,DefaultModelBinder 都會忠實地建立一個新的 Product 執行個體并直接綁定其值。 當綁定程式完成所有工作後,它将建立一個與圖 5 中代碼相似的對象圖表。

  圖 5 從遞歸模型綁定建立的對象圖表

new Product {
      Child = new Product { 
        Child = new Product {
          Child = new Product {
            Child = new Product {
              Child = new Product {
                Child = new Product {
                  Name = "MADNESS!"
                }
              }
            }
          }
        }
      }
    }
                  

  盡管精心設計的上述示例僅設定了一個屬性的值,但卻很好地示範了 DefaultModelBinder 遞歸模型綁定功能如何允許它支援一些非常複雜的現有對象圖表。 如果你可以建立表單字段名表示要填充的值,通過使用遞歸模型綁定,不管該值位于對象層次結構中的哪個位置,模型綁定程式都會找到并綁定它。

  模型綁定不适用的情況

  實際情況: 存在一些 DefaultModelBinder 無法綁定的模型。 然而,還存在預設模型綁定邏輯看似無法運作、但實際上隻要使用得當仍可正常運作的情況,并且這種情況相當普遍。

  下面提供了開發人員往往認為 DefaultModelBinder 無法處理的一些最常見的情況,并說明了如何僅使用 DefaultModelBinder 來實作這些情況。

  複雜集合 現有的 ASP.NET MVC 值提供程式将所有請求字段名稱視為表單釋出值對待。 例如,表單釋出中的原始值集合,其中每個值都需要其自己的唯一索引(添加了空格以增強可讀性):

MyCollection[0]=one &
    MyCollection[1]=two &
    MyCollection[2]=three
                  

  相同方法還可應用于複雜對象集合。 要示範此功能,請通過将 UnitPrice 屬性更改為 Currency 對象集合來更新 Product 類以便支援多種貨币:

public class Product : IProduct
    {
      public IEnumerable<Currency> UnitPrice { get; set; }
     
      // ...
              }
                  

  更改之後,需要下面的請求參數來填充更新後的 UnitPrice 屬性:

UnitPrice[0].Code=USD &
    UnitPrice[0].Amount=100.00 &
     
    UnitPrice[1].Code=EUR &
    UnitPrice[1].Amount=73.64
                  

  仔細觀察綁定複雜對象集合所需的請求參數的命名文法。 請注意,該區域中用于辨別各個唯一項的索引器和每個執行個體的每個屬性都必須包含該執行個體已編制索引的完整引用。 請記住,模型綁定程式要求屬性名稱遵循表單釋出命名文法,而不管請求是 GET 還是 POST。

  盡管有些不合邏輯,但是 JSON 請求具有相同要求,它們也必須遵循表單釋出命名文法。 例如,前面的 UnitPrice 集合的 JSON 負載。 用于該資料的純 JSON 數組文法應表示為:

[ 
      { "Code": "USD", "Amount": 100.00 },
      { "Code": "EUR", "Amount": 73.64 }
    ]
                  

  但是,預設值提供程式和模型綁定程式要求将資料表示為 JSON 表單釋出:

{
      "UnitPrice[0].Code": "USD",
      "UnitPrice[0].Amount": 100.00,
     
      "UnitPrice[1].Code": "EUR",
      "UnitPrice[1].Amount": 73.64
    }
                  

  複雜對象集合情況可能是開發人員遇到的問題最多的情況之一,因為不是所有開發人員都必須了解該文法。 然而,在你了解了相對較簡單的文法來釋出複雜集合之後,處理這些情況要容易得多。

  通用自定義模型綁定器盡管 DefaultModelBinder 的功能強大到幾乎能夠處理你要處理的所有事情,但是它有時候也不能滿足你的需求。 發生這些情況時,許多開發人員抓住機會使用模型綁定架構的可擴充性模型,并建構其自己的自定義模型綁定程式。

  例如,即使 Microsoft .NET Framework 為面向對象的原則提供卓越的支援,但是 DefaultModelBinder 仍不支援綁定到抽象基類和接口。 要示範這種缺陷,請重構 Product 類以使其派生自包含隻讀屬性的名為 IProduct 的接口。 同樣,更新 Create 控制器操作以使其接受新的 IProduct 接口,而不是具體的 Product 實作,如圖 6 所示。

  圖 6 綁定到接口

public interface IProduct
    {
      DateTime AvailabilityDate { get; }
      int CategoryId { get; }
      string Description { get; }
      ProductKind Kind { get; }
      string Name { get; }
      decimal UnitPrice { get; }
      int UnitsInStock { get; }
    }
     
    public ActionResult Create(IProduct product)
    {
      // ...
              }
                  

  盡管圖 6 中顯示的更新後的 Create 操作是完全合法的 C# 代碼,但會導緻 DefaultModelBinder 引發異常: “無法建立接口的執行個體。”由于 DefaultModelBinder 無法得知要建立的 IProduct 的具體類型,是以模型綁定程式引發此異常完全合乎情理。

  解決該問題的最簡單的方法是建立實作 IModelBinder 接口的自定義模型綁定程式。 圖 7 顯示了 ProductModelBinder,即知道如何建立和綁定 IProduct 接口執行個體的自定義模型綁定程式。

  圖 7 ProductModelBinder(緊密耦合的自定義模型綁定程式)

public class ProductModelBinder : IModelBinder
    {
      public object BindModel
        (
          ControllerContext controllerContext,
          ModelBindingContext bindingContext
        )
      {
        var product = new Product() {
          Description = GetValue(bindingContext, "Description"),
          Name = GetValue(bindingContext, "Name"),
      }; 
     
        string availabilityDateValue = 
          GetValue(bindingContext, "AvailabilityDate");
     
        if(availabilityDateValue != null)
        {
          DateTime date;
          if (DateTime.TryParse(availabilityDateValue, out date))
          product.AvailabilityDate = date;
        }
     
        string categoryIdValue = 
          GetValue(bindingContext, "CategoryId");
     
        if (categoryIdValue != null)
        {
          int categoryId;
          if (Int32.TryParse(categoryIdValue, out categoryId))
          product.CategoryId = categoryId;
        }
     
        // Repeat custom binding code for every property
        // ...
              return product;
      }
     
      private string GetValue(
        ModelBindingContext bindingContext, string key)
      {
        var result = bindingContext.ValueProvider.GetValue(key);
        return (result == null) ?
              null : result.AttemptedValue;
      }
    }
                  

  建立直接實作 IModelBinder 接口的自定義模型綁定程式的缺點是,這些綁定程式經常僅為了修改幾個邏輯區域而複制大量 DefaultModelBinder。 此外,這些自定義綁定程式的常見問題還包括:側重于特定模型類、在架構和業務層之間建立緊密耦合,以及限制重複使用以支援其他模型類型。

  要在你的自定義模型綁定程式中避免所有這些問題,請考慮從 DefaultModelBinder 派生并覆寫特定行為以滿足你的需求。 此方法通常可以為兩個領域的提供最佳功能。

  抽象模型綁定程式嘗試使用 DefaultModelBinder 将模型綁定應用到接口的唯一問題是它不知道如何确定具體的模型類型。 請考慮更進階别的目标: 針對非具體類型開發控制器操作并動态确定每個請求的具體類型的功能。

  通過從 DefaultModelBinder 派生并僅覆寫确定目标模型類型的邏輯,你不僅可以滿足特定 IProduct 方案的需求,還可以實際建立可處理大多數其他接口層次結構的通用模型綁定程式。 圖 8 顯示通用模型抽象模型綁定程式的示例。

  圖 8 通用抽象模型綁定程式

public class AbstractModelBinder : DefaultModelBinder
    {
      private readonly string _typeNameKey;
     
      public AbstractModelBinder(string typeNameKey = null)
      {
        _typeNameKey = typeNameKey ?? "
              __type__";
      }
     
      public override object BindModel
      (
        ControllerContext controllerContext,
        ModelBindingContext bindingContext
      )
      {
        var providerResult =
        bindingContext.ValueProvider.GetValue(_typeNameKey);
     
        if (providerResult != null)
        {
          var modelTypeName = providerResult.AttemptedValue;
     
          var modelType =
            BuildManager.GetReferencedAssemblies()
              .Cast<Assembly>()
              .SelectMany(x => x.GetExportedTypes())
              .Where(type => !type.IsInterface)
              .Where(type => !type.IsAbstract)
              .Where(bindingContext.ModelType.IsAssignableFrom)
              .FirstOrDefault(type =>
                string.Equals(type.Name, modelTypeName,
                  StringComparison.OrdinalIgnoreCase));
     
          if (modelType != null)
          {
            var metaData =
            ModelMetadataProviders.Current
            .GetMetadataForType(null, modelType);
     
            bindingContext.ModelMetadata = metaData;
          }
        }
     
        // Fall back to default model binding behavior
        return base.BindModel(controllerContext, bindingContext);
      }
    }
                  

  要支援接口的模型綁定,模型綁定程式必須首先将接口轉換為具體類型。 為此,AbstractModelBinder 從請求的值提供程式請求“__type__”鍵。 對此類型的資料使用值提供程式可以在定義“__type__”值的範圍内提供靈活性。 例如,該鍵可以定義為路由的一部分(在路由資料中),指定為查詢字元串參數,或者甚至表示為表單釋出資料中的字段。

  接下來,AbstractModelBinder 使用具體類型名稱生成一組新的中繼資料,以描述具體類的詳細資訊。   AbstractModelBinder 使用該新的中繼資料取代描述初始抽象模型類型的現有 ModelMetadata 屬性,有效地導緻模型綁定程式忘記它在一開始曾綁定到非具體類型。

  在 AbstractModelBinder 使用綁定到正确模型所需的所有資訊取代模型中繼資料後,它會完全将控制權交還給基本的 DefaultModelBinder 邏輯以使其處理其餘工作。

  AbstractModelBinder 是一個很好的示例,示範了如何通過直接從 IModelBinder 接口派生,使用你自己的自定義邏輯擴充預設綁定邏輯,而不需要重複進行基本的工作。

  模型綁定程式選擇

  建立自定義模型綁定程式僅僅是第一步。 要在你的應用程式中應用自定義模型綁定邏輯,你還必須注冊自定義模型綁定程式。 大多數教程都向你示範了兩種注冊自定義模型綁定程式的方法。

  全局 ModelBinders 集合覆寫特定類型的模型綁定程式的一般推薦方法是将類型到綁定程式的映射注冊到 ModelBinders.Binders 字典。

  下面的代碼段通知架構使用 AbstractModelBinder 綁定 Currency 模型:

  1.           ModelBinders.Binders.Add(typeof(Currency), new AbstractModelBinder());

覆寫預設模型綁定程式或者,你也可以通過将模型綁定程式配置設定給 ModelBinders.Binders.DefaultBinder 屬性來替換全局預設處理程式。 例如:

ModelBinders.Binders.DefaultBinder = new AbstractModelBinder();
                  

  盡管這兩種方法适用于許多情況,但是 ASP.NET MVC 還允許你使用其他方法為類型注冊模型綁定程式: 屬性和提供程式。

  使用自定義屬性修飾模型

  除将類型映射添加到 ModelBinders 字典以外,ASP.NET MVC 架構還提供抽象 System.Web.Mvc.CustomModelBinderAttribute,該屬性允許你為應用該屬性 (attribute) 的每個類或屬性 (property) 動态建立模型綁定程式。 圖 9 顯示建立 AbstractModelBinder 的 CustomModelBinderAttribute 實作。

  圖 9 CustomModelBinderAttribute 實作

[AttributeUsage(
      AttributeTargets.Class | AttributeTargets.Enum |
      AttributeTargets.Interface | AttributeTargets.Parameter |
      AttributeTargets.Struct | AttributeTargets.Property,
      AllowMultiple = false, Inherited = false
    )]
    public class AbstractModelBinderAttribute : CustomModelBinderAttribute
    {
      public override IModelBinder GetBinder()
      {
        return new AbstractModelBinder();
      }
    }
                  

  然後,你可以将 AbstractModelBinderAttribute 應用到任何模型類或屬性,例如:

public class Product
    {
      [AbstractModelBinder]
      public IEnumerable<CurrencyRequest> UnitPrice { get; set; }
      // ...
              }
                  

  現在,當模型綁定程式嘗試為 Product.UnitPrice 查找合适的綁定程式時,它将發現 AbstractModelBinderAttribute 并使用 AbstractModelBinder 綁定 Product.UnitPrice 屬性。

  利用自定義模型綁定程式屬性為使用更具聲明性的方法配置模型綁定程式,同時簡化全局模型綁定程式集合提供了一種絕佳方法。 此外,自定義模型綁定程式屬性可以應用于所有類和單個屬性的事實意味着你可以精确控制模型綁定過程。

  問綁定程式!

  模型綁定程式提供程式提供實時執行任意代碼以确定指定類型的最佳可能模型綁定程式的功能。 是以,他們在用于單個模型類型的顯式模型綁定程式注冊、基于屬性的靜态注冊和用于所有類型的已設定預設模型綁定程式之間提供了絕佳的中間地帶。

  下面的代碼示範了如何建立為所有端口和抽象類型提供 AbstractModelBinder 的 IModelBinderProvider:

public class AbstractModelBinderProvider : IModelBinderProvider
    {
      public IModelBinder GetBinder(Type modelType)
      {
        if (modelType.IsAbstract || modelType.IsInterface)
          return new AbstractModelBinder();
     
        return null;
      }
    }
                  

  訓示是否将 AbstractModelBinder 應用到指定模型類型的邏輯則相對較簡單: 該類型是否為非具體類型? 如果是,AbstractModelBinder 則是适用于該類型的模型綁定程式,是以請執行個體化模型綁定程式并将其傳回。 如果該類型是一個具體類型,AbstractModelBinder 不适用;請傳回空值以訓示模型綁定程式與該類型不比對。

  實作 .GetBinder 邏輯時需要記住的重要一點是将針對作為模型綁定候選項的所有屬性執行該邏輯,是以請務必精簡該邏輯,否則很容易為你的應用程式帶來性能問題。

  為了開始使用模型綁定程式提供程式,将其添加到在 ModelBinderProviders.BinderProviders 集合中維護的提供程式清單。 例如,按如下方式注冊 AbstractModelBinder:

var provider = new AbstractModelBinderProvider();
    ModelBinderProviders.BinderProviders.Add(provider);
                  

  這非常簡單,你已經在整個應用程式中為非具體類型添加了模型綁定支援。

  模型綁定方法将确定适當的模型綁定程式的任務從架構轉移到最合适的位置 — 模型綁定程式本身,進而使模型綁定選擇 更具有動态性。

  關鍵擴充點

  與任何其他方法一樣,ASP.NET MVC 模型綁定允許控制器操作接受複雜的對象類型作為參數。 此外,模型綁定分離填充對象的邏輯和使用填充對象的邏輯,進而有助于更好地分離問題。

  我已探究了模型綁定架構中一些關鍵擴充點,可以幫助你充分利用該架構。 花時間了解 ASP.NET MVC 模型綁定以及如何正确使用它可以對所有應用程式(甚至是最簡單的應用程式)帶來重大影響。

  Jess Chadwick 是一名專攻 Web 技術的獨立軟體顧問。 他具有 10 多年的開發經驗,其涉及範圍從新興企業的嵌入式裝置到财富 500 強公司的企業級 Web 場。 他是一名 ASP 專家、微軟 ASP.NET 最佳職員,以及書籍和雜志作者。 他積極參與開發社團,經常在使用者組和會議中發言,并上司 NJDOTNET 紐澤西州中部 .NET 使用者組。

  衷心感謝以下技術專家對本文進行了審閱:Phil Haack

繼續閱讀