在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,并为项目制定一个强名称密钥文件:

image

重新编译CMSProjectTemplateWizard,然后打开Visual Studio 2010 Command Prompt工具,在命令提示符中使用gacutil.exe将编译出来的程序集安装到GAC中:

image

现在我们已经创建了一个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按钮:

image

在Visual Studio 2010完成了项目的创建后,我们得到如下的解决方案:

image

编译CMSTest1解决方案,我们发现,我们的CMSTest1解决方案已经被成功编译:

image

双击打开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的布局大致如下:

image

修改窗体的后台代码,添加一个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在创建项目之前会给出一个对话框,提示用户是否需要立即编译:

image

细心的朋友会发现,结合场景一和场景二的应用,我们就可以为用户提供一个动态参数输入的界面,而在项目模板中使用这个参数。

场景三:动态创建解决方案文件夹(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来实现一个安装包,以便用户能够直接安装并使用我们的模板。这部分内容我会在下一篇文章中重点介绍。

本文案例下载

 

posted @ 2012-01-18 20:17  dax.net  阅读(11472)  评论(17编辑  收藏  举报