zoukankan      html  css  js  c++  java
  • 在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来实现一个安装包,以便用户能够直接安装并使用我们的模板。这部分内容我会在下一篇文章中重点介绍。

    本文案例下载

  • 相关阅读:
    【纯水题】POJ 1852 Ants
    【树形DP】BZOJ 1131 Sta
    【不知道怎么分类】HDU
    【树形DP】CF 1293E Xenon's Attack on the Gangs
    【贪心算法】CF Emergency Evacuation
    【思维】UVA 11300 Spreading the Wealth
    【树形DP】NOI2003 逃学的小孩
    【树形DP】BZOJ 3829 Farmcraft
    【树形DP】JSOI BZOJ4472 salesman
    【迷宫问题】CodeForces 1292A A NEKO's Maze Game
  • 原文地址:https://www.cnblogs.com/daxnet/p/2325928.html
Copyright © 2011-2022 走看看