添加项目向导






4.46/5 (9投票s)
一个可以无需停止 Visual Studio 即可配置的向导。
目录
引言
开发 Visual Studio 的向导并不难,但有些事情很繁琐,需要一些研究。对于您想添加到 Visual Studio .NET 的每个新向导,每次修改代码并需要测试时,您都将面临启动和停止 Visual Studio 的新实例;不用说,这并不实用。
添加项向导旨在解决所有这些问题,并用一个通用向导替换您需要的任何(简单)向导,该向导可以添加新的内部向导,并且可以在不停止当前运行的 Visual Studio 的情况下更改现有的内部向导。只有当您需要执行超出简单模板插入的功能时,才需要编程新向导。
安装向导
第 1 步。
如果您打开解决方案并生成设置项目,您可以使用生成的 MSI 来实际安装向导,或者只需右键单击 AddItemWizardSetup 项目并选择“Install”。安装向导将弹出。
第 2 步。
输入安装向导的路径。此路径相对于 Visual Studio 的安装路径。如果您不清楚,请不要担心;如果您尝试在不正确的文件夹中安装,安装程序将显示正确的路径。遗憾的是,我还没有找到一种方法可以在此时之前检查并相应地更改安装路径。如果有人知道答案,请告诉我。
步骤 3。
按照安装程序中的所有必要步骤进行操作,安装完成后,启动 Visual Studio .NET 的新实例并打开您想处理的任意解决方案,然后继续下一个主题。
使用向导
步骤 1:调用向导
与其他向导一样,只需右键单击项目并选择“Add -> Add New Item...”即可。
这将显示包含所有向导及其图标的熟悉窗口。我将我的添加到第一个位置,不是为了强加,只是因为我比其他任何向导都更频繁地使用它。如果您不喜欢它在第一个位置,我稍后会告诉您如何更改它的位置。
双击添加项向导,以显示添加项向导自己的用户界面。
每个内部向导都可以拥有自己的图像(200 x 200 像素),因为图片胜过千言万语。当没有选择内部向导,或者未为选定的内部向导指定图像时,将显示默认图像(见上文)。
步骤 2:选择将一个或多个文件添加到项目的实际内部向导。
双击所需的内部向导,将显示一个动态构建的界面,具体取决于您选择的特定内部向导的 *Wizards.xml* 中占位符的数量。
我稍后将详细解释所有内容的配置方式。内部向导界面将如下所示:
对于内部向导将添加到您的项目的每个文件,都会创建一个选项卡页面。在每个选项卡页面上,您至少会看到文件名和一个额外的文本框,用于处理与该文件相关的任何补充占位符。
步骤 3:将文件添加到您的项目。
当输入完此内部向导所需的所有强制数据后,“Add”按钮将被启用:按此按钮即可将文件添加到您的项目。添加的第一个文件将成为解决方案中活动的那个,因此您可以立即开始编写一些代码。添加窗体类时,请按“Form”按钮(或通常按 SHIFT+F7)进入窗体设计模式。请记住,如果这是添加到项目中的第一个窗体,则必须添加对 *System.Windows.Forms.dll* 和 *System.Drawing.dll* 的引用,否则设计器将无法显示。
配置向导
Wizards.xml 文件
请查看 Visual Studio .NET 的安装文件夹,通常为:
C:\Program Files\Microsoft Visual Studio .NET 2003\
在此文件夹中,您将找到一个名为“*VC#*”的 C# 文件夹,然后在其中有一个名为“*VC#Wizards*”的所有向导的文件夹。在后者中,您会找到一个在安装 Add-Item 向导时创建的文件夹:“*AddItemWizard*”。查看此文件夹,您会发现三样东西:
- 一个“*WizardImages*”文件夹,其中包含所有图像,每个内部向导一个。
- 一个“*WizardItems*”文件夹,其中包含所有模板,每个内部向导文件一个模板。
- “*Wizards.xml*”:Add-Item 向导的主配置文件。
让我们看一下 *Wizards.xml* 文件(的一部分)。
<?xml version="1.0" encoding="utf-8" ?>
<Wizards>
<AddItem name="AbstractClass.cs"
description="Add an abstract class"
image="WizardImages\...">
<File path="WizardItems\AbstractClass.cs">
<PlaceHolder id= "ClassName"
readOnly="false" mandatory="true" />
<PlaceHolder id= "FileName" readOnly="false"
mandatory="true"
dynamicValue="string.Concat([[Class Name]],".cs")"/>
</File>
</AddItem>
...
</Wizards>
内部向导定义在 <Wizards>
和 </Wizards>
标签之间,对于每个内部向导,都需要在 <AddItem>
和 </AddItem>
标签之间定义一个项。每个 AddItem
标签都有一个 name
和一个 description
属性,以及一个可选的 image
属性。如果没有指定图像,则该内部向导将显示默认图像(见前文)。每个内部向导至少需要一个在 <File>
和 </File>
标签之间定义的文件(毕竟,向导就是用来添加文件的)。
每个文件在生成的向导 UI 中都有自己的选项卡页面。文件有一个 `path` 属性,指向将项目添加到项目时使用的实际模板。
接下来是占位符列表。每个占位符都必须有一个唯一的 ID,并用于替换指向的模板中相应占位符的出现。您至少需要一个占位符:“File Name”,它用于生成添加到项目的实际文件。
还有四个属性定义了占位符:
- 一个 '
readOnly
' 属性,定义对应的输入文本框是否只读(true 或 false)。 - 一个 '
mandatory
' 属性,定义此文本框是否需要填写(true 或 false)。 - 一个可选的 '
defaultValue
' 属性,定义对应文本框的初始值。 - 一个可选的 '
dynamicValue
' 属性,用于从同一文件的其他占位符值或此内部向导不同文件的占位符构建此文本框的值。
动态值
因此,在 dynamicValue
中,您实际上是在编写 C# 代码!奇怪的是,在处理字符串或需要其他特殊字符时,您必须使用特殊的“" 符号。
例如
<PlaceHolder id="File Name" ...
dynamicValue="string.Concat([[Class Name]],".cs")" />
这将通过将“Class Name”文本框的值与文字值“.cs”连接起来来确定“File Name”文本框的值。如您所见,动态值中的占位符由双重 [[ ]] 符号指定。当更改用于确定此类值的文本框的值时,动态值将始终重新计算。因此,在此示例中,当您在“Class Name”文本框中输入类名时,“File Name”的值会随之更改,就像您同时在两个文本框中输入一样。如果您想包含来自该内部向导的另一个文件的占位符,请在双重 {{ }} 符号中指定该文件的“path”。像这样:
dynamicValue="string.Concat({{WizardItems\FileTemplate.cs}}
[[Class Name]],".Designer.cs")"/>
每次通过选择内部向导来调用它时,都会编译动态值,这就是为什么在实际 UI 弹出之前会注意到轻微延迟的原因。这样做的好处是,通过修改配置文件并重试,可以轻松调整内部向导。如果代码无法编译,添加项向导将显示一个包含问题(们)的窗口。
正如您所见,我将您在 dynamicValue
属性中输入的代码包装在一个类中,该类位于唯一的命名空间内,并带有一个方法。在这里,您已经可以看到添加项向导的内部工作原理,但稍后在代码部分将有更多介绍。让我们稍微多关注配置。
内部向导文件模板
每个内部向导中的文件条目都指向由 path
属性指定的模板,让我们看一下这样的模板:
using System;
namespace [!Namespace]
{
// Class : [!Class Name]
// Author : [!AuthorName]
//
// Modification History
// --------------------
// Name | Date | Description
// --------+-------------+---------------------------------
// [!AuthorCode]#001 | [!Today(dd-MMM-yyyy)] | Creation
// --------+-------------+---------------------------------
public class [!Class Name]
{
#region Constants
#endregion
#region Private fields
#endregion
#region Constructors
public [!Class Name]()
{
}
#endregion
#region Public Methods
#endregion
#region Public Properties
#endregion
#region Private Methods
#endregion
}
}
熟悉现有向导模板的人会注意到我使用完全相同的语法来指定占位符。对于不熟悉的人来说,语法是:[!Placeholder Name]
。
固定占位符
添加项向导有一些固定的占位符:
[!Namespace]
:命名空间占位符。[!AuthorName]
:此处将放置作者姓名,作者姓名在 *WizardAuthorDefinition.xml* 文件中指定(见下文)。[!AuthorCode]
:作者代码,也在 *WizardAuthorDefinition.xml* 文件中指定。[!Today(dateFormat)]
:将被替换为DateTime.Today
,格式由dateFormat
指定,它可以是任何合法的格式,不要指定任何引号。
模板中使用的所有其他占位符都应在 *Wizards.xml* 配置文件中指定为占位符。要获得一个好的例子,请查看 *Wizards.xml* 中 TypedCollection 向导的定义及其对应的模板。
作者定义文件
*WizardAuthorDefinition.xml* 文件可以在以下文件夹中找到:*..\Microsoft Visual Studio .NET 2003\VC#\VC#Wizards*。它之所以是一个独立的文件,是因为我有其他特定于我的工作的向导也使用了同一个文件。
更改向导在添加项对话框中的图标位置
既然我之前承诺过:现在就来实现。安装向导时,一个 vsdir 文件会被放置在以下文件夹中:*..\Microsoft Visual Studio .NET 2003\VC#\CSharpProjectItems*。
所以,如果您想改变添加项向导将自己放在第一个位置的事实;请编辑其 vsdir 文件“*AddItemWizard.vsz*”;其内容如下:
..\AddItemWizard.vsz| |Add-item Wizard|1|
Wizard containing other wizards| | | |New.unknown
将此处显示的粗体数字 **1** 更改为任何较大的值,以了解要使用什么值。您需要检查同一文件夹中的所有其他 vsdir 文件及其所有向导定义。
关注点
向导图标,将在添加项对话框中显示
我花了一段时间才弄清楚如何在添加项对话框中为向导显示自定义图标,结果发现解决方法很简单(如果您已经搜索了这么久)。
在“*\\Microsoft Visual Studio .NET 2003\VC#\CSharpProjectItems*”文件夹中,每个向导还有一个 vsz 文件。现在,您要做的就是将一个与您的 vsz 文件同名但扩展名为 *.ico* 的图标放入同一文件夹中。在 vsdir 文件本身中,保留所有列,对于所有标准向导,用 # 符号后跟一个数字表示,留空;如上面的 vsdir 规范所示。
查找 Visual Studio .NET 的安装位置。
为了找到 Visual Studio .NET 的安装位置,我浏览了注册表,试图找到一个可以给出安装路径的键,并用它来确定向导应该安装的路径。实际上我第一次弄错了。由于我安装了 VS2005 Beta,我发现我当时使用的键不再有效(抱歉,记不清是哪个了)。我现在使用的键是:
VisualStudio.Solution.7.0\Shell\open\Command
:适用于 Visual Studio .NET 2001VisualStudio.Solution.7.1\Shell\open\Command
:适用于 Visual Studio .NET 2003VisualStudio.c.8.0\shell\Open\Command
:适用于 Visual Studio .NET 2005
注册您的程序集
编写向导或插件时,还有另一个棘手的部分。您需要为 COM 互操作注册您的 DLL。否则,在尝试调用代码时,您会收到一些不清楚的消息,例如:
- 类未注册
- 未知异常
当您尝试注册类并且收到“没有可注册的类型”消息时,请确保要为 COM 互操作注册的类的构造函数没有参数。
某些类型(例如只有常量的类)可能无法访问:在类上方使用属性 `[ComVisible( true )
]` 应该可以解决此问题。
程序集在安装过程中注册(这就是为什么您可能看到一个命令窗口闪过)。要了解如何完成,请查看 `WizardInstall` 类中的 `RegisterDlls()` 方法。
修复 Visual Studio 问题。
安装向导或插件时,有时(非常罕见,但确实会发生)Visual Studio 的某些功能,如创建新解决方案或将新项目添加到解决方案,将不再工作并抛出与上述类似的漂亮消息。这可能是由于我们自己注册了一些类,并且 VS-DLL 的其他注册被某种程度地搞砸了。
幸运的是,这个问题可以通过重新注册那些 Visual Studio DLL 来修补。需要注册的 DLL 是:
- extensibility.dll
- VSLangProj.dll
它们都可以在“*..\Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE\PublicAssemblies*”文件夹中找到。您还需要对“*C:\Program Files\Common Files\Microsoft Shared\MSEnv\vslangproj.tlb*”文件执行 RegTlb 命令。RegTlb 命令可以在“*C:\WINDOWS\system32\Regtlb*”文件夹中找到。
补丁是在 `WizardInstaller` 类中注册向导 DLL 之后,通过 `PatchVisualStudioProblem()` 方法完成的。
代码
运行时生成、编译和执行代码。
由于已经有很多关于向导的文章,我将重点介绍这个向导的特殊部分:运行时编译和执行指向生成新代码的代码结构的代码。
向导窗体构造函数。
每当从添加项向导主窗体中选择一个向导时,就会像这样创建一个新的 `WizardForm`:
public WizardForm( Wizard selectedWizard )
{
InitializeComponent();
SelectedWizard = selectedWizard;
this.Text = " - " + selectedWizard.Name;
labelDescription.Text = selectedWizard.Description;
CreateDynamicCode();
AddDynamicControls();
CheckButtonAddEnabled();
}
如您所见,选定的内部向导被传递到构造函数,并存储在 `SelectedWizard` 字段中,该字段定义为:
// This field needs to be a public static field
// so that the dynamically generated classes
// can point to it.
// Do not just change its name, its used within "" to generate the dynamic code !!
public static Wizard SelectedWizard;
我想,我在这里给自己留下的备注已经说明了一切。
下一步是为每个占位符创建所需的动态代码:
private void CreateDynamicCode()
{
int fileNr = 0;
foreach( AddItemFile addFile in SelectedWizard.Files )
{
foreach( PlaceHolder placeHolder in addFile.PlaceHolders )
{
CreateDynamicCode( placeHolder, addFile, fileNr );
}
fileNr++;
}
}
希望在 C# 的未来版本中能有一个带有初始化语句和迭代语句的 `foreach` 构造!:)
CreateDynamicCode 方法
private void CreateDynamicCode
( PlaceHolder placeHolderToProcess
, AddItemFile addFile
, int fileToProcess
)
{
if( placeHolderToProcess.DynamicValue.Length > 0 )
{
string dynamicValue = placeHolderToProcess.DynamicValue;
// Replace the references to the placeholder values in the dynamic value,
// by the actual value of that placeholder
int iOtherFile = 0;
foreach( AddItemFile fileToAdd in SelectedWizard.Files )
{
if( iOtherFile == fileToProcess )
{
int placeHolderNr = 0;
foreach( PlaceHolder placeHolder in addFile.PlaceHolders )
{
string replacer =
PlaceHolder.PlaceholderValueStart
+ placeHolder.Id
+ PlaceHolder.PlaceholderValueEnd;
dynamicValue = dynamicValue.Replace
( replacer
, "WizardForm.SelectedWizard.Files["
+ fileToProcess.ToString()
+ "].PlaceHolders["
+ placeHolderNr.ToString()
+ "].CurrentValue"
);
placeHolderNr++;
}
}
else
{
int placeHolderNr = 0;
foreach( PlaceHolder placeHolder in fileToAdd.PlaceHolders )
// Placeholders of other file !!
{
string replacer;
replacer = PlaceHolder.PlaceholderFileStart
+ fileToAdd.TemplatePath
+ PlaceHolder.PlaceholderFileEnd
+ PlaceHolder.PlaceholderValueStart
+ placeHolder.Id
+ PlaceHolder.PlaceholderValueEnd;
// Here we point to the other file !! =
// iOtherFile ( not fileToProcess )
dynamicValue = dynamicValue.Replace
( replacer
, "WizardForm.SelectedWizard.Files["
+ iOtherFile.ToString()
+ "].PlaceHolders["
+ placeHolderNr.ToString()
+ "].CurrentValue"
);
placeHolderNr++;
}
}
iOtherFile++;
}
// Build the code of the class.
CodeBuilder codeBuilder = new CodeBuilder();
codeBuilder.AppendLine( "using System;" );
codeBuilder.AppendLine( "using System.IO;" );
codeBuilder.AppendEmtyLine();
codeBuilder.AppendLine( "using Erlend.String;");
codeBuilder.AppendEmtyLine();
codeBuilder.AppendLine( "using ", AddItemWizard.Namespace, ";" );
codeBuilder.AppendEmtyLine();
string nameSpace =
AddItemWizard.Namespace
+ "."
+ Path.GetFileNameWithoutExtension( addFile.TemplatePath )
+ "."
+ XString.RemoveAllSpaces( placeHolderToProcess.Id );
codeBuilder.OpenNamespace( nameSpace );
codeBuilder.AppendLine( "public class ", RunTimeClassName );
codeBuilder.OpenScope();
codeBuilder.AppendLine( "public object ", RunTimeMethodName, "()" );
codeBuilder.OpenScope();
codeBuilder.AppendLine( "return ", dynamicValue, ";" );
codeBuilder.CloseScope();
codeBuilder.CloseScope();
codeBuilder.CloseNamespace();
placeHolderToProcess.DynamicClass = CompileMethod
( "Placeholder '"
+ placeHolderToProcess.Id
+ @"' has invalid C# code in the 'dynamicValue'"
+ @" attribute of the 'WizardConfig\Wizards.xml' file"
, codeBuilder.ToString()
, RunTimeClassName, nameSpace
);
}
}
在这里,*Wizards.xml* 文件中指定的 `dynamicValue` 属性被处理,其中存在的所有占位符都被替换为实际存储该占位符值的变量的名称(最终是相应文本框中保存的值)。
在此之后,`dynamicValue` 被插入到一个代码片段中,该片段是我使用自己的 `codeBuilder` 类构建的,它只是一个 `stringBuilder` 加上一些额外的东西。也许我可以使用 CodeDOM,但我对此一无所知,而且这也不是这个项目的重点。无论如何,我认为选择的解决方案非常易读。
编译方法
接下来,代码被编译,然后实例化生成的类并存储在占位符的 `DynamicClass` 属性中,供以后执行。
private object CompileMethod
( string errorFormTitle
, string code
, string generateClass
, string nameSpace
)
{
object runTimeClass = null;
ICodeCompiler compiler =
new CSharpCodeProvider().CreateCompiler();
CompilerParameters compilerParameters =
new CompilerParameters();
compilerParameters.ReferencedAssemblies.Add(
"System.dll" );
compilerParameters.ReferencedAssemblies.Add(
"System.Windows.Forms.dll" );
compilerParameters.ReferencedAssemblies.Add(
Path.Combine( AddItemWizard.WizardFolder,
"Erlend.String.dll") );
compilerParameters.ReferencedAssemblies.Add(
Path.Combine( AddItemWizard.WizardFolder,
AddItemWizard.Namespace + ".dll" ) );
CompilerResults compilerResults =
compiler.CompileAssemblyFromSource( compilerParameters , code );
if (compilerResults.Errors.HasErrors)
{
CompilationErrors.Show( errorFormTitle, code,
compilerResults.Errors );
}
else
{
Assembly compiledAssembly = compilerResults.CompiledAssembly;
runTimeClass = compiledAssembly.CreateInstance( nameSpace +
"." + generateClass );
}
return runTimeClass;
}
在设置的编译器参数中,添加了对 *system.dll* 的引用,也添加了我自己的字符串库的引用,该库为您提供了我抽象类 `XString` 中找到的大量补充静态字符串操作方法(幸运的是,C# 2.0 将有静态类的可能性)。另一个引用的 DLL 是我自己的 DLL,即添加项向导本身的 DLL,因此您生成的代码可以使用实际生成代码的程序的属性和方法!幸运的是,否则这将永远不会奏效。如果您查看生成的代码(在 `CreateDynamicCode` 方法中,处理 `dynamicValue` 的部分),您会看到代码现在指向类似 `WizardForm.SelectedWizard.Files[..]` 的内容,这是一个包含所有文件和占位符、它们的值以及动态生成类本身的结构。这样,生成的代码就能获得实际占位符的值。
编译成功后,类将被实例化并返回,存储在 `DynamicClass` 属性中。
处理占位符。
现在,每当文本框的值发生变化时,就会执行以下方法来更新依赖的占位符和文本框:
private void ProcessDependantPlaceHolderValues( PlaceHolder changedPlaceHolder )
{
foreach( Control control in addedInputControls )
{
if( control != null )
{
// Each control points to its corresponding placeholder
PlaceHolder placeHolder = (PlaceHolder)control.Tag;
// A placeholder may not be dependant on itself
if( placeHolder != changedPlaceHolder )
{
// if the dependancy list of the control's placeholder points to
// the changed placeholder; its code has to be executed.
foreach( string placeHolderId in placeHolder.DependancyList )
{
if( placeHolderId == changedPlaceHolder.Id )
{
// Execute the dynamic code to update the placeholder currentValue
// Copy the new value in this control.Text;
if ( placeHolder.DynamicClass != null )
{
try
{
string newValue = (string)
placeHolder.DynamicClass.GetType().InvokeMember(
RunTimeMethodName, BindingFlags.InvokeMethod,
null, placeHolder.DynamicClass, null);
control.Text = placeHolder.CurrentValue = newValue;
}
catch( Exception e )
{
MessageBox.Show
( "An exception has been thrown in dyanmic"
+ " generated code of placeHolder : "
+ placeHolderId
+ Environment.NewLine
+ e.Message
+ ( ( e.InnerException != null )? Environment.NewLine
+ "InnerException : " + e.InnerException.Message : "" )
);
}
}
}
}
}
}
}
}
在哪里生成要添加的文件。
当用户添加新项时,他们可以选择在项目或该项目中的任何文件夹或子文件夹中进行。我曾认为找到用户选择添加文件的位置会比实际情况简单得多。事实证明,没有特殊的方法或属性可以提供此信息。您需要做的是遍历解决方案树,以找出树中的哪个项是选定的。每个人都知道在添加文件或签出文件时,解决方案树会像圣安德烈亚斯断层一样摇晃,这就是原因:遍历树,并在遍历过程中,为经过的每个元素更新树(...)。为了避免这种情况并加快过程(任何不必要的 I/O 到屏幕的操作都会减慢速度,当然),最好在搜索之前隐藏解决方案树,并在之后重新显示它。
private string GetCorrectFolder()
{
// Get the the Solution Explorer tree
UIHierarchy uiHSolutionExplorer ;
EnvDTE.Window explorerWindow =
wizardInfo.Dte.Windows.Item( Constants.vsext_wk_SProjectWindow );
// Hide the tree to speed thing up and prevent it from shaking.
explorerWindow.Visible = false;
uiHSolutionExplorer = (UIHierarchy)explorerWindow.Object;
// Get the top node (the name of the solution)
UIHierarchyItem uiHSolutionRootNode ;
uiHSolutionRootNode = uiHSolutionExplorer.UIHierarchyItems.Item(1);
// Search the tree recursively
string folder = SearchTree( uiHSolutionRootNode );
if( folder.Length > 0 )
{
// remove the first, this is the project folder,
// otherwise we have twice the projectfolder.
int pos = folder.IndexOf(@"\" );
folder = ( pos != -1 ) ? folder.Substring( pos+1 ) : "";
}
// Make it visible again could be nice.
explorerWindow.Visible = true;
return folder;
}
private string SearchTree( UIHierarchyItem uiHItem )
{
if( uiHItem.UIHierarchyItems.Count > 0 )
{
foreach( UIHierarchyItem subItem in uiHItem.UIHierarchyItems )
{
if( subItem.IsSelected )
{
return subItem.Name;
}
else
{
string foundFolder = SearchTree( subItem );
if( foundFolder.Length > 0 )
return subItem.Name + @"\" + foundFolder;
}
}
}
return "";
}
结论
使用添加项向导,您可以在将文件添加到解决方案时节省大量时间。由于向导配置功能强大且一旦您习惯了就非常简单,您就会进行配置。我使用该向导已经大约半年了,它几乎成了我将文件添加到解决方案时唯一使用的向导。
如果您发现任何错误,请报告,我会尽快修复。如果您有任何改进建议,我很乐意研究它们。
如果将来有足够的时间,我将撰写另一篇文章,介绍我的 Visual Studio .NET(2001 和 2003)的可插拔插件框架,该框架附带了大约 50 个插件。但由于这个项目规模庞大,我需要大量时间,所以不要太快抱有希望。
历史
- 2006年1月2日:删除了在非英语版 Windows 上安装时发生的错误(无法安装)。