在Visual Studio 2010中创建多项目(解决方案)模板【二】
在上文中我给大家介绍了多项目解决方案模板的创建,在文章的最后我们遇到了一个问题,就是$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(至目前为止,未完成)