在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來實現一個安裝包,以便用戶能夠直接安裝並使用我們的模板。這部分內容我會在下一篇文章中重點介紹。

本文案例下載

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM