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

    本文案例下载

  • 相关阅读:
    erlang中变量作用域
    erlang数字转字符串
    gen_server笔记
    Using Eredis, Redis With Erlang
    erlang lists模块函数使用大全
    erlang抽象码与basho的protobuf
    Erlang Module and Function
    Erlang中频繁发送远程消息要注意的问题
    Erlang中的record与宏
    数据结构之数羊问题
  • 原文地址:https://www.cnblogs.com/daxnet/p/2325928.html
Copyright © 2011-2022 走看看