在上文中我給大家介紹了多項目解決方案模板的創建,在文章的最后我們遇到了一個問題,就是$safeprojectname$這個模板參數(宏)所指代的意義在各個項目中都不一樣,而我們卻希望它能夠簡單地指代用戶所輸入的項目名稱。本文將從這個問題出發,討論在Visual Studio 2010中是如何使用Template Wizard來設計復雜的多項目解決方案的。
Template Wizard的基本應用
創建Template Wizard項目
在CMSProjectTemplate解決方案下,新建一個C# Class Library,取名為CMSProjectTemplateWizard,在該項目上添加Microsoft.VisualStudio.TemplateWizardInterface以及EnvDTE的引用(注意:此時需要將EnvDTE的Embed Interop Types設置為False),然后新建一個名為RootWizardImpl的類,使其繼承於Microsoft.VisualStudio.TemplateWizard.IWizard接口,然后實現該接口中的方法。RootWizardImpl類的代碼如下:
public class RootWizardImpl : IWizard
{
private string safeprojectname;
private static Dictionary<string, string> globalParameters = new Dictionary<string, string>();
public static IEnumerable<KeyValuePair<string, string>> GlobalParameters
{
get { return globalParameters; }
}
#region IWizard Members
public void BeforeOpeningFile(EnvDTE.ProjectItem projectItem) { }
public void ProjectFinishedGenerating(EnvDTE.Project project) { }
public void ProjectItemFinishedGenerating(EnvDTE.ProjectItem projectItem) { }
public void RunFinished() { }
public void RunStarted(object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind, object[] customParams)
{
safeprojectname = replacementsDictionary["$safeprojectname$"];
globalParameters["$safeprojectname$"] = safeprojectname;
}
public bool ShouldAddProjectItem(string filePath) { return true; }
#endregion
}
在上面的代碼中,我們僅實現了RunStarted方法,在這個方法中,我們首先通過replacementsDictionary將“根項目”(也就是對Visual Studio而言的那個單一項目)的$safeprojectname$的值取出,然后將其放到一個靜態字典集合globalParameters中,這個globalParameters會在后面子項目的TemplateWizard中使用,以替代子項目中$safeprojectname$的值。
順便說一下RunStarted方法的幾個參數:
- automationObject:DTE的自動化對象,它可以被轉換成DTE接口的實例,以便在代碼中操作Visual Studio IDE
- replacementsDictionary:包含了所有內嵌的和自定義的模板參數(宏),這些參數值會在項目完成創建時,替換掉項目各個文件中所出現的與之對應的參數(宏)
- WizardRunKind:指代Template Wizard的執行類型,比如是創建Item Template、Project Template還是Multiple-Project Template
- customParams:包含了來自vstemplate文件的自定義參數。在vstemplate文件中,可以在WizardData XML節點下設置這些自定義的值
現在,讓我們繼續在CMSProjectTemplateWizard項目中新建一個名為ChildWizardImpl的類,同樣讓其繼承於Microsoft.VisualStudio.TemplateWizard.IWizard接口,具體代碼如下:
public class ChildWizardImpl : IWizard
{
#region IWizard Members
public void BeforeOpeningFile(EnvDTE.ProjectItem projectItem) { }
public void ProjectFinishedGenerating(EnvDTE.Project project) { }
public void ProjectItemFinishedGenerating(EnvDTE.ProjectItem projectItem) { }
public void RunFinished() { }
public void RunStarted(object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind, object[] customParams)
{
string safeprojectname = RootWizardImpl.GlobalParameters.Where(p => p.Key == "$safeprojectname$").First().Value;
replacementsDictionary["$safeprojectname$"] = safeprojectname;
}
public bool ShouldAddProjectItem(string filePath) { return true; }
#endregion
}
接下來,我們需要對CMSProjectTemplateWizard進行數字簽名,可以直接在項目上直接單擊鼠標右鍵,選擇Properties,在打開的項目屬性標簽頁上選擇Signing,並為項目制定一個強名稱密鑰文件:
重新編譯CMSProjectTemplateWizard,然后打開Visual Studio 2010 Command Prompt工具,在命令提示符中使用gacutil.exe將編譯出來的程序集安裝到GAC中:
現在我們已經創建了一個Template Wizard項目,接下來,我們需要調整CMSProjectTemplate的設置,使其能夠使用已創建的Template Wizard
在CMSProjectTemplate中使用Template Wizard
打開CMSProjectTemplate.vstemplate文件,在文件的底部TemplateContent節點之后加入WizardExtension節點,設置節點的內容如下:
<WizardExtension> <Assembly>CMSProjectTemplateWizard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=52319e57efa35eb8</Assembly> <FullClassName>CMSProjectTemplateWizard.RootWizardImpl</FullClassName> </WizardExtension>
逐一打開CMSProjectTemplate\CMSTemplate下的所有子目錄,修改每個目錄下的MyTemplate.vstemplate文件,在文件的底部TemplateContent節點之后加入WizardExtension節點,設置節點的內容如下:
<WizardExtension> <Assembly>CMSProjectTemplateWizard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=52319e57efa35eb8</Assembly> <FullClassName>CMSProjectTemplateWizard.ChildWizardImpl</FullClassName> </WizardExtension>
重新編譯CMSProjectTemplate項目,並將編譯輸出的ZIP文件復制到<User_Documents>\Visual Studio 2010\Templates\ProjectTemplates\Visual C#目錄下。
重新測試CMSProjectTemplate
現在讓我們重新新建一個CMSProjectTemplate的項目,在Visual Studio 2010中單擊File –> New –> Project菜單,在彈出的對話框中選擇CMSProjectTemplate,並輸入項目名稱然后單擊OK按鈕:
在Visual Studio 2010完成了項目的創建后,我們得到如下的解決方案:
編譯CMSTest1解決方案,我們發現,我們的CMSTest1解決方案已經被成功編譯:
雙擊打開IoCFactory.cs文件,我們發現,代碼中已經使用了正確的命名空間,整個解決方案的$safeprojectname$已經保持一致:
namespace CMSTest1.Infrastructure
{
public static class IoCFactory
{
public static T GetObject<T>()
{
// TODO: Implement the IoC/DI logic here.
return default(T);
}
}
}
至此,我們事實上已經成功地創建了一個多項目解決方案的模板,用戶已經可以開始使用這個模板來新建一個類似RainbowCMS的解決方案了。
Template Wizard的高級應用
現在,讓我們看看Template Wizard的幾個高級應用的例子以及使用中需要注意的問題。
場景一:通過Template Wizard向CMSProjectTemplate傳遞自定義參數
這個應用場景比較簡單,假設我們需要通過Template Wizard向CMSProjectTemplate傳遞一個名為$nowyear$的參數,表示當前日期的年份,基本步驟如下:
- 在RootWizardImpl的RunStarted方法中,向replacementsDictionary中添加一個$nowyear$的項,值為DateTime.Now.Year.ToString()
- 在RootWizardImpl的RunStarted方法中,同樣向globalParameters中添加一個$nowyear$的項,值為DateTime.Now.Year.ToString()
- 在ChildWizardImpl的RunStarted方法中,通過RootWizardImpl從GlobalParameters中取得$nowyear$的值,並將其賦給replacementsDictionary
現在就可以在CMSProjectTemplate的任意地方使用$nowyear$參數,當項目被創建時,該參數會被當前日期的年份替換。
場景二:為用戶提供“創建解決方案后編譯”的選項
在CMSProjectTemplateWizard中,新建一個Windows Form,然后在這個Form上添加一個復選框,設置其文本為“Build the solution after it is created.”,表示當用戶選中這個復選框時,在完成解決方案創建之后,需要Visual Studio 2010立即對該解決方案進行編譯。這個Form的布局大致如下:
修改窗體的后台代碼,添加一個BuildSolutionRequired屬性,代碼如下:
public bool BuildSolutionRequired
{
get { return this.chkBuild.Checked; }
}
向CMSProjectTemplateWizard項目添加EnvDTE80的引用,修改RootWizardImpl類,將其改為:
public class RootWizardImpl : IWizard
{
private bool buildSolutionRequired;
private string safeprojectname;
private EnvDTE80.DTE2 dteObject;
private static Dictionary<string, string> globalParameters = new Dictionary<string, string>();
public static IEnumerable<KeyValuePair<string, string>> GlobalParameters
{
get { return globalParameters; }
}
#region IWizard Members
public void BeforeOpeningFile(EnvDTE.ProjectItem projectItem) { }
public void ProjectFinishedGenerating(EnvDTE.Project project) { }
public void ProjectItemFinishedGenerating(EnvDTE.ProjectItem projectItem) { }
public void RunFinished()
{
EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dteObject.Solution;
if (buildSolutionRequired)
solution.SolutionBuild.Build();
}
public void RunStarted(object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind, object[] customParams)
{
try
{
dteObject = (automationObject as EnvDTE80.DTE2);
safeprojectname = replacementsDictionary["$safeprojectname$"];
globalParameters["$safeprojectname$"] = safeprojectname;
frmOptions options = new frmOptions();
if (options.ShowDialog() == DialogResult.OK)
{
buildSolutionRequired = options.BuildSolutionRequired;
}
}
catch (Exception ex) { MessageBox.Show(ex.ToString()); }
}
public bool ShouldAddProjectItem(string filePath) { return true; }
#endregion
}
重新編譯CMSProjectTemplateWizard,並將其重裝到GAC,然后嘗試新建一個CMSProjectTemplate的項目,Visual Studio在創建項目之前會給出一個對話框,提示用戶是否需要立即編譯:
細心的朋友會發現,結合場景一和場景二的應用,我們就可以為用戶提供一個動態參數輸入的界面,而在項目模板中使用這個參數。
場景三:動態創建解決方案文件夾(Solution Folder)
通常,我們都會在Template Wizard執行完成之后,動態創建解決方案文件夾(Solution Folder)。假設我們需要在解決方案中添加一個名為ReferencedProjects文件夾,我們可以在RootWizardImpl.RunFinished方法中添加如下代碼:
public void RunFinished()
{
EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dteObject.Solution;
Project refProjectsFolderProject = solution.AddSolutionFolder("ReferencedProjects");
}
場景四:在解決方案文件夾下引用已經存在的項目文件
在場景三中,我們已經在解決方案下創建了一個ReferencedProjects文件夾,現在更進一步,將一個已存在於C:\Test目錄下的C#項目文件Test.csproj添加到這個文件夾下。基於場景三中的代碼,我們修改RunFinished方法如下:
public void RunFinished()
{
EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dteObject.Solution;
Project refProjectsFolderProject = solution.AddSolutionFolder("ReferencedProjects");
EnvDTE80.SolutionFolder refProjectsSolutionFolder =
(EnvDTE80.SolutionFolder)refProjectsFolderProject.Object;
string csprojFileName = @"C:\Test\Test.csproj";
refProjectsSolutionFolder.AddFromFile(csprojFileName);
}
場景五:Project GUID問題的解決
這個問題描述起來有點點復雜,總的來說,雖然我們可以在CMSProjectTemplate項目中,在所包含的csproj文件中將ProjectGuid節點的值設置為$guid1$等,但在最終產生的項目文件上,我們發現,Visual Studio 2010會自動重新生成一個GUID來覆蓋我們所指定的這個。換句話說,即使是在RootWizardImpl.RunFinished方法中,也得不到這個最終的Project GUID。通常情況下,這不是什么大問題,因為一般我們也不太關心這個ProjectGuid究竟用什么值,因為項目之間的引用也是通過項目名稱實現的。比如在我們的CMSProjectTemplate中就不存在這樣的問題。然而有些第三方的項目類型或許就會使用Project GUID來實現項目引用,比如大名鼎鼎的Windows Installer XML Toolset(WiX),它就是根據Project GUID來決定其所關聯的項目的,這樣就出現問題了:在WiX項目的模板中,我們可以給定其引用的項目的GUID,但在最后生成的解決方案中,被引用的這個項目的GUID發生了變化,導致WiX項目無法對所需的項目進行引用,用戶需要手動地重新添加項目引用,這樣做就達不到自動化項目創建的目的。
這個問題我上網研究了很長時間,網上也沒有找到合適的辦法,很多國外技術社區的朋友也在一直抱怨為什么Visual Studio 2010在創建解決方案的時候需要重新產生Project GUID。最后經過我的反復試驗,我找到了解決這個問題的辦法。既然我們無法修改被引用項目的Project GUID,那么我們就直接在WiX項目上動手,在WiX項目中將它所設置的Project GUID替換為被引用項目的最終Project GUID。如何確定這個被引用項目的最終的Project GUID呢?只需要在解決方案資源管理器中找到這個被引用的項目,然后執行Save操作,項目的Project GUID就會被確定下來,然后再使用文本讀取等手段獲得這個最終的Project GUID即可。詳細代碼如下:
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using System.Xml;
using EnvDTE;
using Microsoft.VisualStudio.TemplateWizard;
public void RunFinished()
{
// 獲取Solution對象
EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dteObject.Solution;
Project webProject = null;
Project wixProject = null;
foreach (Project p in solution.Projects)
{
if (p.Name == string.Format("{0}.Web", safeprojectname))
{
webProject = p;
}
if (p.Name == string.Format("{0}.Wix", safeprojectname))
{
wixProject = p;
}
}
// 保存web項目,使得其Project GUID能夠被最終確定下來.
webProject.Save();
// 保存需要修改的WiX項目,以確保“保存項目”對話框不會彈出.
wixProject.Save();
// 在解決方案資源管理器中定位WiX項目
Window solutionExplorerWindow = dteObject.ToolWindows.SolutionExplorer.Parent as Window;
solutionExplorerWindow.Activate();
UIHierarchyItem solutionHier = dteObject.ToolWindows.SolutionExplorer.UIHierarchyItems.Item(1);
UIHierarchyItem wixProjectHier = null;
foreach (UIHierarchyItem item in solutionHier.UIHierarchyItems)
{
if (item.Name == string.Format("{0}.Wix", safeprojectname))
{
wixProjectHier = item;
break;
}
}
if (wixProjectHier != null)
{
// 在解決方案資源管理器中將WiX項目選中
wixProjectHier.Select(vsUISelectionType.vsUISelectionTypeSelect);
// 將WiX項目從解決方案中卸載(Unload)
dteObject.ExecuteCommand("Project.UnloadProject");
// 調用ReplaceProjectGuid方法,修改WiX項目中對web項目
// 的引用Guid
ReplaceProjectGuid(webProject, wixProject);
// 稍等片刻...
System.Threading.Thread.Sleep(500);
// 重新加載WiX項目
dteObject.ExecuteCommand("Project.ReloadProject");
}
}
private void ReplaceProjectGuid(Project webProject, Project wixProject)
{
var webProjectFullName = webProject.FullName;
var webProjectText = File.ReadAllText(webProjectFullName);
int pos = webProjectText.IndexOf("<ProjectGuid>", StringComparison.InvariantCultureIgnoreCase);
var guid = webProjectText.Substring(pos + "<ProjectGuid>".Length, 38);
var wixProjectFullName = wixProject.FullName;
XmlDocument xmlDoc = new XmlDocument();
XmlNamespaceManager namespaceMgr = new XmlNamespaceManager(xmlDoc.NameTable);
namespaceMgr.AddNamespace("ns", "http://schemas.microsoft.com/developer/msbuild/2003");
xmlDoc.Load(wixProjectFullName);
XmlNode node = xmlDoc.SelectSingleNode("//ns:Project//ns:ItemGroup[3]//ns:ProjectReference[2]//ns:Project", namespaceMgr);
node.InnerText = guid;
xmlDoc.Save(wixProjectFullName);
}
總結
至此,我們已經成功地借助Template Wizard創建了一個多項目解決方案的模板,我們還學習了Template Wizard的一些高級應用。但我們的CMSProjectTemplate還沒有全部完成,我們還需要為其提供一個更好聽的名字、更好看的圖標,而且我們還希望能夠通過Visual Studio 2010 Extension來實現一個安裝包,以便用戶能夠直接安裝並使用我們的模板。這部分內容我會在下一篇文章中重點介紹。
本文案例下載
- CMSProjectTemplate(至目前為止,未完成)







