[T4] 使用T4文字範本輸出所有列舉(Enum)類別至Javascript檔案

使用T4文字範本輸出所有列舉(Enum)類別至Javascript檔案

前言

 

最近專案前端畫面複雜且充斥著許多邏輯判斷,而這些邏輯判斷依據多是各下拉式選單之選取項目,因此在javascript 中就會存在著許多魔術數字(如圖所示),相當令人討厭且可讀性差;由於這些已知選單項目都會對應至預先定義的Enum類別中,所以筆者希望透過T4協助來把專案中前端需要使用的Enum類別轉換成Javascript Enum檔案,也就是希望抽出這些魔術數字,讓前端在判斷選項數值所代表之意義時,有個統一的參考點。

 

image

 

 

實作

 

首先於專案中新增T4檔案

 

image

 

接著實作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檔案中。

 

image

 

稍微測試一下

 

方案內Domain專案中存在BarrierType與SettleMode列舉(Enum)類別

 

image

 

內容如下

 

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檔案

 

image

 

 

輸出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://t4-editor.tangible-engineering.com/blog/walking-the-visual-studio-code-model-with-t4-template.html

http://stackoverflow.com/questions/16672480/generate-javascript-representation-of-enums


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !