2016 年 12 月

第 31 卷,第 13 期

本文章是由機器翻譯。

Roslyn - 使用 Roslyn 及 T4 範本產生 JavaScript

Nick Harrison | 2016 年 12 月

有一天我的女兒告訴我笑話 smartphone 和電話的功能之間的交談有關。它會像這樣︰ 功能已說到功能電話智慧型手機? 「 我是從未來,您可以瞭解我嗎? 」 有時候它覺得這樣時可以學習到新的和剪下邊緣。Roslyn 來自未來,而且很難了解剛開始的時候。

在本文中,我將討論 Roslyn,可能不會收到盡焦點時給予它應有的方式。我將著重在產生 T4 與 JavaScript 使用 Roslyn 為中繼資料的來源。這會使用工作區 API 的一些語法 API、 符號 API 和 T4 引擎的執行階段範本。產生的實際 JavaScript 是次要了解用來收集中繼資料的處理程序。

Roslyn 也提供一些不錯的選項,來產生程式碼,因為您可能會認為這兩項技術會衝突,並不合作無間。其沙箱重疊,但這兩項技術可以搭而時,通常相衝突的技術。

等一下,T4 是什麼?

2015年書簡潔 Syncfusion 數列中,「 T4 簡單地說,「 如果您熟悉 T4,提供您需要的所有背景 (bit.ly/2cOtWuN)。

現在,最重要的是,要知道是 T4 是 Microsoft 的範本為基礎的文字轉換工具組。摘要至範本的中繼資料和文字會變成您想要的程式碼。實際上,不限於程式碼。您可以產生任何類型的文字,但原始碼是最常見的輸出。您可以產生 HTML、 SQL、 文字文件、 Visual Basic.NET、 C# 或任何文字為基礎的輸出。

看看**[圖 1**。它會顯示一個簡單的主控台應用程式。在 Visual Studio 中,我會加入名為 AngularResourceService.tt 的新執行階段文字範本。範本程式碼會自動產生將實作範本在執行階段,您可以在主控台視窗中看到一些 C# 程式碼。

使用 T4 設計階段程式碼產生
[圖 1 使用 T4 設計階段程式碼產生

本文中,我將告訴您如何使用 Roslyn 收集中繼資料從摘要 T4 產生 JavaScript 類別,並接著使用 Roslyn 來新增 Web API 專案的 JavaScript 回至方案。

就概念而言,處理流程看起來就像**[圖 2**。

T4 程序流程
[圖 2 T4 程序流程

Roslyn 摘要 T4

產生程式碼是中繼資料需求極大的程序。您需要的中繼資料描述您要產生的程式碼。反映、 程式碼模型和資料字典是隨手可得的中繼資料的常見原因。Roslyn 可以提供您會收到來自反映或程式碼模型,但沒有這種方法造成的問題的所有中繼資料。

在本文中我將使用 Roslyn 尋找衍生自 ApiController 類別。我將使用 T4 範本來建立每個控制站的 JavaScript 類別,並公開每個動作的方法,並與控制器相關聯的 ViewModel 中每個屬性的屬性。結果將看起來中的程式碼**[圖 3**。

[圖 3 執行結果的程式碼

var app = angular.module("challenge", [ "ngResource"]);
  app.factory(ActivitiesResource , function ($resource) {
    return $resource(
      'http://localhost:53595//Activities',{Activities : '@Activities'},{
    Id : "",
    ActivityCode : "",
    ProjectId : "",
    StartDate : "",
    EndDate : "",
  , get: {
      method: "GET"
    }
  , put: {
      method: "PUT"
    }
  , post: {
      method: "POST"
    }
  , delete: {
      method: "DELETE"
    }
  });
});

收集的中繼資料

我開始在 Visual Studio 2015 中建立新的主控台應用程式專案蒐集中繼資料。在此專案中,我將有專用於收集與 Roslyn,以及使用 T4 範本的中繼資料類別。這是執行階段範本會產生一些 JavaScript 程式碼,根據收集的中繼資料。

一旦建立專案時,會發出下列命令從封裝管理員主控台︰

Install-Package Microsoft.CodeAnalysis.CSharp.Workspaces

這可確保 CSharp 編譯器的最新 Roslyn 程式碼,並使用相關的服務。

我的各種方法的程式碼置於名 RoslynDataProvider 的新類別。我會將這個類別整篇文章,我想要收集的 Roslyn 的中繼資料時,它將是方便的參考。

我使用 MSBuildWorksspace 來取得工作區中,會提供編譯所需的所有內容。方案之後,我可以輕鬆地逐步尋求 WebApi 專案的專案︰

private Project GetWebApiProject()
{
  var work = MSBuildWorkspace.Create();
  var solution = work.OpenSolutionAsync(PathToSolution).Result;
  var project = solution.Projects.FirstOrDefault(p =>
    p.Name.ToUpper().EndsWith("WEBAPI"));
  if (project == null)
    throw new ApplicationException(
      "WebApi project not found in solution " + PathToSolution);
  return project;
}

如果您遵循不同的命名慣例,您可以輕易地將它併入 GetWebApiProject,尋找您感興趣的專案。

現在,我知道我想要使用的專案,我必須取得編譯該專案中,因為我要用來識別相關的控制站類型的參考。我需要編譯,因為我要用來判斷是否類別是衍生自 System.Web.Http.ApiController 的 SemanticModel。從專案中,我可以取得包含在專案中的文件。每份文件是類別的不同的檔案,其中包括一個以上的類別宣告中,雖然很好的最佳作法,在任何檔案中包含單一類別,以及具有名稱的檔案相符名稱;但不是每個人都隨時會遵循此標準。

尋找控制器

[圖 4示範如何在每個文件中找出所有的類別宣告,並判斷是否類別衍生自 ApiController。

[圖 4 尋找專案中的控制器

public IEnumerable<ClassDeclarationSyntax> FindControllers(Project project)
{
  compilation = project.GetCompilationAsync().Result;
  var targetType = compilation.GetTypeByMetadataName(
    "System.Web.Http.ApiController");
  foreach (var document in project.Documents)
  {
    var tree = document.GetSyntaxTreeAsync().Result;
    var semanticModel = compilation.GetSemanticModel(tree);
    foreach (var type in tree.GetRoot().DescendantNodes().
      OfType<ClassDeclarationSyntax>()
      .Where(type => GetBaseClasses(semanticModel, type).Contains(targetType)))
    {
      yield return type;
    }
  }
}

編譯具有存取權的所有編譯專案所需的參考,因為它將不會有問題解決目標類型。當我編譯物件,我已啟動編譯專案,但要中斷中途透過一旦取得所需的中繼資料的詳細資料。

[圖 5顯示擔負重責大任,來判斷目前的類別衍生自目標類別是否 GetBaseClasses 方法。這並不是絕對必要多的處理。若要判斷是否要將類別衍生自 ApiController,我根本不在乎過程中,實作之介面的相關,但是藉由這些詳細資料,這會變成可用於各種不同的位置方便的公用程式方法。

[圖 5 尋找基底類別和介面

public static IEnumerable<INamedTypeSymbol> GetBaseClasses
  (SemanticModel model, BaseTypeDeclarationSyntax type)
{
  var classSymbol = model.GetDeclaredSymbol(type);
  var returnValue = new List<INamedTypeSymbol>();
  while (classSymbol.BaseType != null)
  {
    returnValue.Add(classSymbol.BaseType);
    if (classSymbol.Interfaces != null)
      returnValue.AddRange(classSymbol.Interfaces);
    classSymbol = classSymbol.BaseType;
  }
  return returnValue;
}

這種分析變複雜使用反映,因為反射方法將會依賴遞迴或可能需要有載入的組件,以取得存取權的所有中介的型別一路任何數目。這種分析不使用程式碼模型,甚至可能,但是使用 SemanticModel Roslyn 是相當簡單。SemanticModel 是珍貴的中繼資料中。它代表編譯器知道有關程式碼經過麻煩的符號繫結語法樹狀目錄的所有項目。除了追蹤基底型別,它可以用來回答艱難的問題,像是多載/覆寫解析或尋找所有參考的方法或屬性或任何符號。

尋找相關聯的模型

此時,我有專案中的所有控制站的存取。在 JavaScript 類別,它也是不錯公開 (expose) 中的模型在控制器中的動作所傳回的屬性。若要了解其運作方式,看看下列程式碼,顯示樣板執行如 WebApi 的輸出︰

public class Activity
  {
    public int Id { get; set; }
    public int ActivityCode { get; set; }
    public int ProjectId { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
  }

在此情況下,樣板執行針對模型中所示**[圖 6**。

[圖 6 產生 API 控制器

public class ActivitiesController : ApiController
  {
    private ApplicationDbContext db = new ApplicationDbContext();
    // GET: api/Activities
    public IQueryable<Activity> GetActivities()
    {
      return db.Activities;
    }
    // GET: api/Activities/5
    [ResponseType(typeof(Activity))]
    public IHttpActionResult GetActivity(int id)
    {
      Activity activity = db.Activities.Find(id);
      if (activity == null)
      {
        return NotFound();
      }
      return Ok(activity);
    }
    // POST: api/Activities
    [ResponseType(typeof(Activity))]
    public IHttpActionResult PostActivity(Activity activity)
    {
      if (!ModelState.IsValid)
      {
        return BadRequest(ModelState);
      }
      db.Activities.Add(activity);
      db.SaveChanges();
      return CreatedAtRoute("DefaultApi", new { id = activity.Id }, activity);
    }
    // DELETE: api/Activities/5
    [ResponseType(typeof(Activity))]
    public IHttpActionResult DeleteActivity(int id)
    {
      Activity activity = db.Activities.Find(id);
      if (activity == null)
      {
        return NotFound();
      }
      db.Activities.Remove(activity);
      db.SaveChanges();
      return Ok(activity);
    }

新增至動作的 ResponseType 屬性將連結到控制器的 ViewModel。您可以使用此屬性,取得與動作相關聯之模型的名稱。只要使用 scaffolding 建立控制器,則會與相同的模型相關聯的每個動作,但是控制器以手動方式建立或編輯之後產生可能因此一致。[圖 7示範如何針對所有的動作,以取得完整清單,萬一發生多個與控制器相關聯的模型進行比較。

[圖 7 尋找與控制器相關聯的模型

public IEnumerable<TypeInfo> FindAssociatedModel
  (SemanticModel semanticModel, TypeDeclarationSyntax controller)
{
  var returnValue = new List<TypeInfo>();
  var attributes = controller.DescendantNodes().OfType<AttributeSyntax>()
    .Where(a => a.Name.ToString() == "ResponseType");
  var parameters = attributes.Select(a =>
    a.ArgumentList.Arguments.FirstOrDefault());
  var types = parameters.Select(p=>p.Expression).OfType<TypeOfExpressionSyntax>();
  foreach (var t in types)
  {
    var symbol = semanticModel.GetTypeInfo(t.Type);
    if (symbol.Type.SpecialType == SpecialType.System_Void) continue;
    returnValue.Add( symbol);
  }
  return returnValue.Distinct();
}

有趣的邏輯中這個方法。某些部分的而難以察覺。請記住 ResponseType 屬性看起來像︰

[ResponseType(typeof(Activity))]

我想要存取的運算式,也就是屬性的第一個參數型別中參考的型別中的屬性,在此情況下,活動。屬性變數是在控制器中找到的 ResponseType 屬性的清單。參數變數是這些屬性的參數清單。每個參數將 TypeOfExpressionSyntax,而且我可以透過 TypeOfExpressionSyntax 物件的型別屬性來取得相關聯的類型。同樣地,SemanticModel 用來提取該型別,可讓您可能想要的所有詳細資料的符號。

Distinct 方法的結尾會確保傳回的每個模型是唯一的。在大部分情況下,您應該會有重複的項目,因為在控制器中的多個動作將會與相同的模型相關聯。它也是用來檢查正在 void ResponseType 好主意。找不到那里任何有趣的屬性。

檢查相關聯的模型

下列程式碼顯示如何尋找所有模型在控制器中找到的屬性︰

public IEnumerable<ISymbol> GetProperties(IEnumerable<TypeInfo> models)
{
  return models.Select(typeInfo => typeInfo.Type.GetMembers()
    .Where(m => m.Kind == SymbolKind.Property))
    .SelectMany(properties => properties).Distinct();
}

尋找動作

除了從相關聯的模型中顯示的屬性,我想要包含在控制器中方法的參考。控制器中的方法的動作。我只想要在公用方法,因為這些是 WebApi 動作時,它們應該全部轉譯成適當的 HTTP 動詞命令。

有幾個不同的慣例,接著處理這種對應。後面接著樣板是以動詞名稱開頭的方法名稱。因此 put 的方法是 PutActivity,post 方法會 PostActivity,delete 方法 DeleteActivity,而通常會有兩個 get 方法︰ GetActivity 和 GetActivities。您可以分辨 get 方法,藉由檢查這些方法的傳回型別之間的差異。Get 方法的傳回型別會直接或間接實作 IEnumerable 介面,如果是 get 所有;否則,它是 get 單一項目的方法。

另一種方法是,您明確地新增屬性至指定的動詞,則此方法可能會有任何名稱。[圖 8 GetActions 所識別的公用方法,然後將它們對應使用這兩種方法的動詞命令會顯示程式碼。

[圖 8 控制站上尋找動作

public IEnumerable<string> GetActions(ClassDeclarationSyntax controller)
{
  var semanticModel = compilation.GetSemanticModel(controller.SyntaxTree);
  var actions = controller.Members.OfType<MethodDeclarationSyntax>();
  var returnValue = new List<string>();
  foreach (var action in actions.Where
        (a => a.Modifiers.Any(m => m.Kind() == SyntaxKind.PublicKeyword)))
  {
    var mapName = MapByMethodName(semanticModel, action);
    if (mapName != null)
      returnValue.Add(mapName);
    else
    {
      mapName = MapByAttribute(semanticModel, action);
      if (mapName != null)
        returnValue.Add(mapName);
    }
  }
  return returnValue.Distinct();
}

GetActions 方法會先嘗試將對應的方法名稱為基礎。如果還是不行,它會再嘗試對應屬性。如果無法對應方法,它將不會包含在清單中的動作。如果您有想要檢查的不同慣例,您可以輕易地將它併入 GetActions 方法。[圖 9顯示 MapByMethodName 和 MapByAttribute 方法的實作。

[圖 9 MapByName 和 MapByAttribute

private static string MapByAttribute(SemanticModel semanticModel,
  MethodDeclarationSyntax action)
{
  var attributes = action.DescendantNodes().OfType<AttributeSyntax>().ToList();
  if ( attributes.Any(a=>a.Name.ToString() == "HttpGet"))
    return IdentifyIEnumerable(semanticModel, action) ? "query" : "get";
  var targetAttribute = attributes.FirstOrDefault(a =>
    a.Name.ToString().StartsWith("Http"));
  return targetAttribute?.Name.ToString().Replace("Http", "").ToLower();
}
private static string MapByMethodName(SemanticModel semanticModel,
  MethodDeclarationSyntax action)
{
  if (action.Identifier.Text.Contains("Get"))
    return IdentifyIEnumerable(semanticModel, action) ? "query" : "get";
  var regex = new Regex("\b(?'verb'post|put|delete)", RegexOptions.IgnoreCase);
  if (regex.IsMatch(action.Identifier.Text))
    return regex.Matches(action.Identifier.Text)[0]
      .Groups["verb"].Value.ToLower();
  return null;
}

這兩種方法啟動藉由明確地搜尋取得動作,並決定哪種類型的"get"方法參考。

如果此動作不是其中一個 「 取得 」,MapByAttribute 會檢查動作使用 Http 啟動的屬性。如果找到,動詞命令可以決定就是直接採用屬性名稱,並在屬性名稱中移除 Http。沒有需要明確檢查每個屬性,以決定要使用的動詞命令。

MapByMethodName 結構類似。第一次檢查之後取得動作,此方法會使用以查看任何其他動詞是否符合規則運算式。如果找到相符項目,您可以從指名的擷取群組取得動詞命令名稱。

這兩個對應方法需要區分取得單一與取得的所有動作,而且都使用下列程式碼所示的 IdentifyEnumerable 方法︰

private static bool IdentifyIEnumerable(SemanticModel semanticModel,
  MethodDeclarationSyntax actiol2n)
{
  var symbol = semanticModel.GetSymbolInfo(action.ReturnType);
  var typeSymbol = symbol.Symbol as ITypeSymbol;
  if (typeSymbol == null) return false;
  return typeSymbol.AllInterfaces.Any(i => i.Name == "IEnumerable");
}

同樣地,SemanticModel 扮演舉足輕重的角色。我可以區別 get 方法,藉由檢查方法的傳回型別。SemanticModel 會傳回為傳回型別繫結的符號。完成這個符號,可以分辨是否傳回型別會實作 IEnumerable 介面。如果此方法會傳回清單<T>或 Enumerable<T>或任何類型的集合,它會實作 IEnumerable 介面。</T> </T>

T4 範本

現在,所有的中繼資料蒐集,就造訪會結合這些片段全部的 T4 範本。我開始先將執行階段文字範本加入至專案。

執行階段文字範本中,執行範本的輸出會將實作所定義的範本並沒有我想要產生的程式碼的類別。大多數的情況下,任何您可以在文字範本,您可以對執行階段文字範本。差別在於範本來產生程式碼的執行方式。文字範本時,Visual Studio 會處理執行範本,以及建立該範本會執行的裝載環境。使用執行階段文字範本,您必須負責設定裝載環境和執行範本項目。這麼多的工作上,但是它也提供了許多進一步控制執行範本的方式,以及您的輸出。它也會移除任何相依性 [Visual Studio。

編輯 AngularResource.tt 並加入程式碼中的,我開始圖 10範本。

[圖 10 初始範本

<#@ template debug="false" hostspecific="false" language="C#" | #>
var app = angular.module("challenge", [ "ngResource"]);
  app.factory(<#=className #>Resource . function ($resource) {
    return $resource('<#=Url#>/<#=className#>',{<=className#> : '@<#=className#>'},{
    <#=property.Name#> : "",
  query : {
    method: "GET"
    , isArray : true
    }
  ' <#=action#>: {
    method: "<#= action.ToUpper()#>
    }
  });
});

根據您的熟悉程度與 JavaScript,這可能是您 — 如果是的話,別擔心。

第一行是在範本指示詞,以及得知 T4 我將撰寫範本的程式碼在 C# 中;其他兩個屬性會忽略執行階段範本,但是為了清楚起見,我要明確指出我有沒有預期的情況下從裝載環境,而且不會保留偵錯中繼檔案。

T4 範本有點像 ASP 網頁。<# and="" #="">標籤分隔到磁碟機的範本的程式碼和文字範本轉換之間。</#><#= #="">標籤分隔是要進行評估並產生程式碼中插入變數取代。</#=>

查看這個範本,您可以看到中繼資料所提供的類別名稱、 URL、 一份屬性和動作的清單。因為這是執行階段範本中有幾件事,我可以簡化應用程式,但先看一下此範本執行時建立的程式碼,這是完成儲存.TT 檔案,或在 [方案總管] 中的檔案上按一下滑鼠右鍵,然後選取 [執行自訂工具。

執行範本的輸出是一個新的類別,與範本相符。更重要,我向下捲動,我就會發現範本也產生基底類別。這是很重要,因為如果我將基底類別移到新的檔案,並明確指定在範本指示詞中的基底類別,將不會再產生,我可以視需要變更這個基底類別。

接下來,我要這個變更範本指示詞︰

<#@ template debug="false" hostspecific="false" language="C#"
  inherits="AngularResourceServiceBase" #>

然後我會將 AngularResourceServiveBase 移至它自己的檔案。當我再次執行範本時,我會看到產生的類別仍然是衍生自相同基底類別,但不會再產生。現在我可以變更任何所需的基底類別。

接下來,我要加入一些新方法和屬性的兩個基底類別,讓您更輕鬆地提供範本的中繼資料。

若要建立新的方法和屬性,我還需要一些新的 using 陳述式︰

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

我要加入內容的 url 和我建立的文件開頭 RoslynDataProvider:

public string Url { get; set; }
public RoslynDataProvider MetadataProvider { get; set; }

使用這些元件之後,我還需要幾個與 MetadataProvider 互動的方法中所示**[圖 11**。

[圖 11 Helper 方法加入至 AngularResourceServiceBase

public IList<ClassDeclarationSyntax> GetControllers()
{
  var project = MetadataProvider.GetWebApiProject();
  return MetadataProvider.FindControllers(project).ToList();
}
protected IEnumerable<string> GetActions(ClassDeclarationSyntax controller)
{
  return MetadataProvider.GetActions(controller);
}
protected IEnumerable<TypeInfo> GetModels(ClassDeclarationSyntax controller)
{
  return MetadataProvider.GetModels(controller);
}
protected IEnumerable<ISymbol> GetProperties(IEnumerable<TypeInfo> models)
{
  return MetadataProvider.GetProperties(models);
}

現在,我有這些方法加入至基底類別,我已經準備好擴充來使用這些範本。查看範本中的變更圖 12

[圖 12 最終版本的範本

<#@ template debug="false" hostspecific="false" language="C#" inherits="AngularResourceServiceBase" #>
var app = angular.module("challenge", [ "ngResource"]);
<#
  var controllers = GetControllers();
  foreach(var controller in controllers)
  {
    var className = controller.Identifier.Text.Replace("Controller", "");
#>    app.facctory(<#=className #>Resource , function ($resource) {
      return $resource('<#=Url#>/<#=className#>',{<#=className#> : '@<#=className#>'},{
<#
    var models= GetModels(controller);
    var properties = GetProperties(models);
    foreach (var property in properties)
    {
#>
      <#=property.Name#> : "",
<#
    }
    var actions = GetActions(controller);
    foreach (var action in actions)
    {
#>
<#
      if (action == "query")
      {
#>      query : {
      method: "GET"

執行範本

因為這是執行階段範本中,我是負責執行範本的環境設定。[圖 13顯示執行範本所需的程式碼。

[圖 13 執行執行階段文字範本

private static void Main()
{
  var work = MSBuildWorkspace.Create();
  var solution = work.OpenSolutionAsync(Path to the Solution File).Result;
  var metadata = new RoslynDataProvider() {Workspace = work};
  var template = new AngularResourceService
  {
    MetadataProvider = metadata,
    Url = @"http://localhost:53595/"
  };
  var results = template.TransformText();
  var project = metadata.GetWebApiProject();
  var folders = new List<string>() { "Scripts" };
  var document = project.AddDocument("factories.js", results, folders)
    .WithSourceCodeKind(SourceCodeKind.Script)
    ;
  if (!work.TryApplyChanges(document.Project.Solution))
    Console.WriteLine("Failed to add the generated code to the project");
  Console.WriteLine(results);
  Console.ReadLine();
}

我可以將範本儲存或執行自訂工具時建立的類別可以直接執行個體化,我可以設定或存取任何公用屬性或呼叫基底類別的任何公用方法。這是如何設定屬性的值。呼叫 TransformText 方法,將會執行範本,並傳回產生的程式碼做為字串。結果的變數會保留產生的程式碼。其餘程式碼處理所產生的程式碼專案中加入新的文件。

不過有這個程式碼問題。AddDocuments 呼叫成功建立文件,並將它放在指令碼] 資料夾。當呼叫 TryApplyChanges 時,它會傳回成功。問題出在當我查看方案中︰ 指令碼] 資料夾中沒有工廠檔案。問題在於,而 factories.js,它是 factories.cs。AddDocument 方法並未設定為接受擴充功能。擴充功能,不論文件將會加入要加入的專案類型。這是產品本身的設計。

因此,程式會執行並產生 JavaScript 類別之後,檔案會在指令碼] 資料夾中。我只需要為從.cs 的將副檔名變更為.js。

總結

大部分的工作完成這裡取得中繼資料的 Roslyn 置中對齊。不論如何,您打算使用此中繼資料,這些相同的做法會很實用。T4 程式碼中,它將繼續是相關的許多地方。如果您想要產生 Roslyn 不支援任何語言的程式碼,T4 是很大的選擇與您輕鬆地納入您的程序。這是文字的很好的因為您可以使用 Roslyn T4 可讓您產生任何類型,可能是文字的 SQL、 JavaScript、 HTML、 CSS 或甚至是文字的單純的文字時,產生的程式碼只有 C# 和 Visual Basic.NET。

最好是產生這些 JavaScript 類別類似的程式碼,因為它們很費時而且容易產生錯誤。它們也符合模式。盡可能不可行時,您會想要盡可能一致地遵循該模式。最重要的是,您想要的方式產生程式碼看起來可能會隨著時間變更,尤其是對一些新的最佳作法,而形成。如果您只需要更新 T4 範本,以將變更為新 「 最佳的方法 」,您更容易遵循新興的最佳作法。但是,如果您需要修改大量的手動產生單調、 冗長的程式碼,您可能需要多個實作 vogue 跟最佳做法是每個反覆項目。


Nick Harrison是軟體顧問與愛心妻子 Tracy Columbia,S.C.,在即時和女兒。 他一直在開發使用.NET 自 2002年建立商務解決方案的完整堆疊。在 Twitter 上與他聯絡:: @Neh123us、,他還會宣告他的部落格文章,已發行的文章和讀出合作。

感謝下列 Microsoft 技術專家來檢閱這份文件︰ James McCaffrey
Dr。James McCaffrey 適用於在美國華盛頓州 Redmond 的 Microsoft Research他曾在數個 Microsoft 產品,包括 Internet Explorer 和 Bing。Dr。可以連線到 McCaffrey jammc@microsoft.com