使用T4文字範本輸出所有列舉(Enum)類別至Javascript檔案
前言
最近專案前端畫面複雜且充斥著許多邏輯判斷,而這些邏輯判斷依據多是各下拉式選單之選取項目,因此在javascript 中就會存在著許多魔術數字(如圖所示),相當令人討厭且可讀性差;由於這些已知選單項目都會對應至預先定義的Enum類別中,所以筆者希望透過T4協助來把專案中前端需要使用的Enum類別轉換成Javascript Enum檔案,也就是希望抽出這些魔術數字,讓前端在判斷選項數值所代表之意義時,有個統一的參考點。
實作
首先於專案中新增T4檔案
接著實作T4文字範本。簡單來說就是透過以下代碼,找尋方案中各專案內符合條件的Enum類別,將這些類別轉換輸出成我們所需的形式;此範例是直接輸出成javascript檔案,當然如果有使用type script的朋友,稍微調整一下就可符合type script的Enum類別形式,享受以強型別方式操作各Enum成員的好處。以下參考。
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="EnvDte" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ output extension=".js" #>
<#
#region Target Enum Filter Conditions
var jsRootNamespace = "myProject";
// target project names (empty : for all projects)
List<string> includeProjects = new List<string>(){
//"T4EnumJsWebApp",
//"Domain"
};
// target enum name spaces (empty : for all enums)
List<string> includeEnumNameSpaces = new List<string>(){
//"Domain.Enums"
};
// target enum full names (empty : for all enums)
List<string> includeFullEnumNames = new List<string>(){
//"Domain.Enums.BarrierType"
};
#endregion
#>
var <#= jsRootNamespace #> = <#= jsRootNamespace #> || {};
(function () {
<#= jsRootNamespace #>.enums = <#= jsRootNamespace #>.enums || {};
<#
// browse every solution's projects
var visualStudio = (Host as IServiceProvider).GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
foreach(EnvDTE.Project project in visualStudio.Solution.Projects)
{
// filter by pre-define project name
if (includeProjects.Count > 0)
{
string projectName = project.Name;
if(!includeProjects.Contains(projectName))
{ continue; }
}
// browse every project's files
foreach(EnvDTE.ProjectItem item in GetProjectItemsRecursively(project.ProjectItems))
{
//string fileName = item.Name;
if (item.FileCodeModel == null) continue;
foreach(EnvDTE.CodeElement elem in item.FileCodeModel.CodeElements)
{
if (elem.Kind == EnvDTE.vsCMElement.vsCMElementNamespace)
{
// filter by pre-define name space
if (includeEnumNameSpaces.Count > 0)
{
string fullEnumNameSpace = elem.FullName;
if(!includeEnumNameSpaces.Contains(fullEnumNameSpace))
{ continue; }
}
foreach (CodeElement innerElement in elem.Children)
{
if (innerElement.Kind == vsCMElement.vsCMElementEnum)
{
// filter by pre-define project name
if (includeFullEnumNames.Count > 0)
{
string fullEnumName = innerElement.FullName;
if(!includeFullEnumNames.Contains(fullEnumName))
{ continue; }
}
// enum
CodeEnum enu = (CodeEnum)innerElement;
// format enum name
string enumName = LowerFirstChar(enu.Name);
#>
<#= jsRootNamespace #>.enums.<#= enumName #> = {
<#
// deal with each enum members
int memberIndex = 0, currentEnumValue = 0;
foreach (CodeElement member in enu.Members)
{
bool isLastChlid = false;
if (memberIndex == enu.Members.Count-1)
{ isLastChlid = true; }
// get enum member
CodeVariable value = member as CodeVariable;
if (value != null) {
string init = value.InitExpression as string;
int unused;
if (!int.TryParse(init, out unused))
{
// not set value for each enum
// use current value + 1 as enum value
currentEnumValue += 1;
init = currentEnumValue.ToString();
}
// update current enum value
currentEnumValue = Convert.ToInt16(init);
// output enum member
string finalComma = isLastChlid ? "" : ",";
WriteLine("\t\t" + LowerFirstChar(value.Name) + ": " + init + finalComma);
}
memberIndex++;
}
#>
};
<#
}
}
}
}
}
}
#>
})();
<#+
public List<EnvDTE.ProjectItem> GetProjectItemsRecursively(EnvDTE.ProjectItems items)
{
var ret = new List<EnvDTE.ProjectItem>();
if (items == null) return ret;
foreach(EnvDTE.ProjectItem item in items)
{
ret.Add(item);
ret.AddRange(GetProjectItemsRecursively(item.ProjectItems));
}
return ret;
}
public string LowerFirstChar(string originalName)
{
// ex. AAA to aaa
// ex. aaa to aaa
// ex. aAABbbCccEnum to aAABbbCccEnum
// ex. AaaBbbCccEnum to aaaBbbCccEnum
// ex. AAABbbCccEnum to aaaBbbCccEnum
string name= originalName;
// get first lower char index
var regex = new Regex("[a-z]", RegexOptions.None);
var firstLowerChar =
name.AsEnumerable()
.Select((word, index) => new {word, index})
.FirstOrDefault(n=> regex.Match(n.word.ToString()).Success);
if (firstLowerChar == null)
{
// all upper chars
name = name.ToLower();
}
else
{
// has lower char
var firstLowerCharIndex = firstLowerChar.index;
if (firstLowerCharIndex > 0)
{
// keep upper char before the first lower char
if (firstLowerCharIndex != 1)
{ firstLowerCharIndex--; }
// adjust name
name = name.Substring(0, firstLowerCharIndex).ToLower()
+ name.Substring(firstLowerCharIndex);
}
}
return name;
}
#>
由於前端不一定會使用所有Enum類別,因此可以透過以下三種方式來限制輸出Enum條件範圍。首先可以依照方案內的專案名稱來限制,表示指定專案中定義的Enum才會輸出;依此類推,我們可以限制指定Namespace及Enum完整名稱。如果都不填寫,則表示把目前方案內所有專案的Enum都輸出至javascript檔案中。
稍微測試一下
方案內Domain專案中存在BarrierType與SettleMode列舉(Enum)類別
內容如下
namespace Domain.Enums
{
public enum BarrierType
{
[Description("American")]
American = 1,
[Description("Discrete")]
Discrete,
[Description("European")]
European
}
}
namespace Domain.Enums
{
public enum SettleMode
{
[Description("Physical")]
Physical = 1,
[Description("Cash")]
Cash
}
}
建立T4且實作完畢存檔時,就會產出myProject.enmus.js檔案
輸出myProject.enmus.js檔案中就會包含Domain專案中定義的BarrierType與SettleMode列舉類別;接著就可以此資訊作為前端開發時,識別選取項目意義之比較對象,進而實作相對應邏輯來執行頁面互動效果。
var myProject = myProject || {};
(function () {
myProject.enums = myProject.enums || {};
myProject.enums.barrierType = {
american: 1,
discrete: 2,
european: 3
};
myProject.enums.settleMode = {
physical: 1,
cash: 2
};
})();
參考資訊
http://stackoverflow.com/questions/16672480/generate-javascript-representation-of-enums
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !