Visual Studio 扩展:从任意 DLL 访问 VS 选项
Visual Studio Extension: Access VS Options from arbitrary DLL
我目前正在开发我的第一个 VS 扩展,它需要为用户提供一些选项。在 https://msdn.microsoft.com/en-us/library/bb166195.aspx 之后,很容易想出我自己的选项页面。但是,我还没有找到如何阅读我的选项。
我的扩展的解决方案结构如下:
MySolution
MyProject (generates a DLL from C# code)
MyProjectVSIX
按照上面引用的教程,我在我的 VSIX 项目中添加了一个 VS Package
并按照描述对其进行了配置。结果,我的选项页面显示在 Tools/Options 下。好的!这是我的 DialogPage
实现:
public class OptionPageGrid : DialogPage
{
private bool myOption = false;
[Category(Options.CATEGORY_NAME)]
[DisplayName("My option")]
[Description("Description of my option.")]
public bool MyOption
{
get { return myOption; }
set { myOption = value; }
}
}
这是我的包裹的头部 class:
[PackageRegistration(UseManagedResourcesOnly = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] [Guid(MyOptionsPage.PackageGuidString)]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
[ProvideOptionPage(typeof(OptionPageGrid), Options.CATEGORY_NAME, Options.PAGE_NAME, 0, 0, true)]
public sealed class MyOptionsPage : Package, IOptions
{
...
但是,我现在想阅读这些选项,并且我想从 MyProject(不依赖于 MyProjectVSIX)进行阅读。这就是我有点迷路的地方。我的第一次尝试是让我的 Package
实现一个 IOptions
接口,并通过从 Package
的构造函数调用静态方法 Options.Register(IOptions)
让它自己注册。这行得通(即命中 Register()
中的断点),但是当我尝试读取选项时,静态 IOptions
实例仍然为空。我的假设是,这是由于代码是从不同的进程执行的(这是我无法控制的)。
经过更多谷歌搜索后,我尝试获取 DTE
对象的实例(如果我理解正确,这将允许我阅读我的选项),但没有成功。我尝试了几种变体,包括 https://msdn.microsoft.com/en-us/library/ee834473.aspx 和
中描述的变体
DTE Dte = Package.GetGlobalService(typeof(DTE)) as DTE;
我总是以空引用结束。
最后,由于教程建议通过 Package
的实例访问选项,我试图找出如何通过某种注册表获取我的 VS Package
的此类实例(我可以将其转换为 IOptions
),但同样没有运气。
谁能给我指出正确的方向?或者甚至无法从非 VSIX 项目访问 VS 选项?
更新:我做了更多的研究,缺少一条信息:我的扩展是一个单元测试适配器。这似乎暗示测试发现代码和测试执行代码来自不同的进程运行,也就是说,我的假设是正确的。
我同时设法访问了我正在 运行 中的 VS 实例的 DTE
对象(我将 post 尽快用我的完整解决方案我的问题已解决),但仍然无法访问选项。事实上,以下代码(从此处复制:https://msdn.microsoft.com/en-us/library/ms165641.aspx)运行良好:
Properties txtEdCS = DTEProvider.DTE.get_Properties("TextEditor", "CSharp");
Property prop = null;
string msg = null;
foreach (EnvDTE.Property temp in txtEdCS)
{
prop = temp;
msg += ("PROP NAME: " + prop.Name + " VALUE: " + prop.Value) + "\n";
}
MessageBox.Show(msg);
但是,如果我将上面的更改如下:
Properties txtEdCS = DTEProvider.DTE.get_Properties(CATEGORY_NAME, PAGE_NAME);
现在代码崩溃了。奇怪的是,我可以在 HKEY_CURRENT_USER\Software\Microsoft\VisualStudio.0Exp_Config\AutomationProperties\My Test Adapter\General
下的注册表中看到我的 属性 类别和页面。搜索我的属性会在 HKEY_CURRENT_USER\Software\Microsoft\VisualStudio.0Exp\ApplicationPrivateSettings\MyProjectVSIX\OptionPageGrid
下显示它们(也许是因为我添加了
OptionPageGrid Page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
Page.SaveSettingsToStorage();
到 Package
的 Initialize()
方法(正如 Matze 所建议的),也许是因为我之前没有看过那里:-))。
那么如何读取我的属性?
如果你想从包中读取选项,你可以通过 VSPackage
的 GetDialogPage
方法请求一个 OptionPageGrid
实例。例如:
var options = (OptionGridPage)this.package.GetDialogPage(typeof(OptionGridPage));
bool b = options.MyOption;
如果您想从另一个应用程序(或无需 Visual Studio 的运行时环境即可使用的程序集)访问这些选项,您可以尝试直接从 Windows 注册表,但注册表项和值可能不存在,除非选项已由 IDE
或您的包写入。您可以通过从包中调用 SaveSettingsToStorage
方法来强制保留选项,例如在第一次加载时:
options.SaveSettingsToStorage();
设置将存储在以下键下:
HKEY_CURRENT_USER\SOFTWARE\Microsoft\VisualStudio.0\DialogPage
其中12.0
表示Visual Studio的版本。在这个键下你会发现一堆子键,它们的名字是 DialogPage
组件类型的全名。每个键包含 属性 个值;在您的情况下,您应该找到一个名为 MyOption
的 REG_SZ
值,其数据值为 True
或 False
。
My assumption is that this is due to the fact that the code is executed from different processes
除非您非常明确地编写其他内容,否则所有 VS 可扩展性代码 运行 都在 devenv.exe 下的单个 AppDomain 中。如果您看到它显示为空,则表示您的注册码没有 运行,或者没有按照您的预期进行。
这里有几个选项:
- 完全避免该问题:尝试重构您的代码,使非 VSIX 项目中的代码不必从环境中读取内容,而是将选项传递给某些方法调用,而 VSIX 项目会执行此操作.这通常是最好的结果,因为它使单元测试变得容易得多。 ("Global state" 总是不利于测试。)
- 让您的非 VSIX 项目只有一个用于设置选项的静态 class,并且您的 VSIX 项目引用非 VSIX 项目并在设置更改时设置它。
- 通过ProvideService属性注册一个服务,在你的非VSIX项目中仍然为你自己的服务类型调用
Package.GetGlobalService
。这有一个主要的警告,即 GetGlobalService 有很多问题;由于涉及的挑战,我通常避免使用该方法。
- 使用 MEF。如果您熟悉 Import/Export 属性,您可以让 VSIX 项目导出在非 VSIX 项目中定义的接口,然后导入它。如果您熟悉依赖注入的概念,这很好,但学习曲线确实是一个学习悬崖。
这个答案是对我第一个答案的补充。当然,从间谍注册表项中读取选项可能是一种肮脏的方法,因为专有 API 的实现将来可能会发生变化,这可能会破坏您的扩展。正如 Jason 提到的,您可以尝试 "avoid the problem entirely";例如,通过削减 reads/writes 选项 from/to 注册表的功能。
DialogPage
class 提供了方法 LoadSettingsFromStorage
和 SaveSettingsToStorage
,它们都是虚拟的,因此您可以覆盖它们并调用您的自定义持久性功能。
public class OptionPageGrid : DialogPage
{
private readonly ISettingsService<OptionPageGridSettings> settingsService = ...
protected override IWin32Window Window
{
get
{
return new OptionPageGridWindow(this.settingsService);
}
}
public override void LoadSettingsFromStorage()
{
this.settingsService.LoadSettingsFromStorage();
}
public override void SaveSettingsToStorage()
{
this.settingsService.SaveSettingsToStorage();
}
}
ISettingsService<T>
接口的实现可以放入一个共享程序集中,供 VSIX 项目和您的独立应用程序(您的测试适配器程序集)引用。设置服务还可以使用 Windows 注册表,或任何其他合适的存储...
public class WindowsRegistrySettingsService<T> : ISettingsService<T>
{
...
}
public interface ISettingsService<T>
{
T LoadSettingsFromStorage();
void SaveSettingsToStorage();
}
VS 测试适配器框架恰好有 API 用于跨进程共享设置。由于根本没有记录任何内容,因此花了一段时间才弄清楚如何使用 API。有关工作示例,请参阅 this GitHub project.
更新:vstest 框架最近由 MS 开源。
我目前正在开发我的第一个 VS 扩展,它需要为用户提供一些选项。在 https://msdn.microsoft.com/en-us/library/bb166195.aspx 之后,很容易想出我自己的选项页面。但是,我还没有找到如何阅读我的选项。
我的扩展的解决方案结构如下:
MySolution
MyProject (generates a DLL from C# code)
MyProjectVSIX
按照上面引用的教程,我在我的 VSIX 项目中添加了一个 VS Package
并按照描述对其进行了配置。结果,我的选项页面显示在 Tools/Options 下。好的!这是我的 DialogPage
实现:
public class OptionPageGrid : DialogPage
{
private bool myOption = false;
[Category(Options.CATEGORY_NAME)]
[DisplayName("My option")]
[Description("Description of my option.")]
public bool MyOption
{
get { return myOption; }
set { myOption = value; }
}
}
这是我的包裹的头部 class:
[PackageRegistration(UseManagedResourcesOnly = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] [Guid(MyOptionsPage.PackageGuidString)]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
[ProvideOptionPage(typeof(OptionPageGrid), Options.CATEGORY_NAME, Options.PAGE_NAME, 0, 0, true)]
public sealed class MyOptionsPage : Package, IOptions
{
...
但是,我现在想阅读这些选项,并且我想从 MyProject(不依赖于 MyProjectVSIX)进行阅读。这就是我有点迷路的地方。我的第一次尝试是让我的 Package
实现一个 IOptions
接口,并通过从 Package
的构造函数调用静态方法 Options.Register(IOptions)
让它自己注册。这行得通(即命中 Register()
中的断点),但是当我尝试读取选项时,静态 IOptions
实例仍然为空。我的假设是,这是由于代码是从不同的进程执行的(这是我无法控制的)。
经过更多谷歌搜索后,我尝试获取 DTE
对象的实例(如果我理解正确,这将允许我阅读我的选项),但没有成功。我尝试了几种变体,包括 https://msdn.microsoft.com/en-us/library/ee834473.aspx 和
DTE Dte = Package.GetGlobalService(typeof(DTE)) as DTE;
我总是以空引用结束。
最后,由于教程建议通过 Package
的实例访问选项,我试图找出如何通过某种注册表获取我的 VS Package
的此类实例(我可以将其转换为 IOptions
),但同样没有运气。
谁能给我指出正确的方向?或者甚至无法从非 VSIX 项目访问 VS 选项?
更新:我做了更多的研究,缺少一条信息:我的扩展是一个单元测试适配器。这似乎暗示测试发现代码和测试执行代码来自不同的进程运行,也就是说,我的假设是正确的。
我同时设法访问了我正在 运行 中的 VS 实例的 DTE
对象(我将 post 尽快用我的完整解决方案我的问题已解决),但仍然无法访问选项。事实上,以下代码(从此处复制:https://msdn.microsoft.com/en-us/library/ms165641.aspx)运行良好:
Properties txtEdCS = DTEProvider.DTE.get_Properties("TextEditor", "CSharp");
Property prop = null;
string msg = null;
foreach (EnvDTE.Property temp in txtEdCS)
{
prop = temp;
msg += ("PROP NAME: " + prop.Name + " VALUE: " + prop.Value) + "\n";
}
MessageBox.Show(msg);
但是,如果我将上面的更改如下:
Properties txtEdCS = DTEProvider.DTE.get_Properties(CATEGORY_NAME, PAGE_NAME);
现在代码崩溃了。奇怪的是,我可以在 HKEY_CURRENT_USER\Software\Microsoft\VisualStudio.0Exp_Config\AutomationProperties\My Test Adapter\General
下的注册表中看到我的 属性 类别和页面。搜索我的属性会在 HKEY_CURRENT_USER\Software\Microsoft\VisualStudio.0Exp\ApplicationPrivateSettings\MyProjectVSIX\OptionPageGrid
下显示它们(也许是因为我添加了
OptionPageGrid Page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
Page.SaveSettingsToStorage();
到 Package
的 Initialize()
方法(正如 Matze 所建议的),也许是因为我之前没有看过那里:-))。
那么如何读取我的属性?
如果你想从包中读取选项,你可以通过 VSPackage
的 GetDialogPage
方法请求一个 OptionPageGrid
实例。例如:
var options = (OptionGridPage)this.package.GetDialogPage(typeof(OptionGridPage));
bool b = options.MyOption;
如果您想从另一个应用程序(或无需 Visual Studio 的运行时环境即可使用的程序集)访问这些选项,您可以尝试直接从 Windows 注册表,但注册表项和值可能不存在,除非选项已由 IDE
或您的包写入。您可以通过从包中调用 SaveSettingsToStorage
方法来强制保留选项,例如在第一次加载时:
options.SaveSettingsToStorage();
设置将存储在以下键下:
HKEY_CURRENT_USER\SOFTWARE\Microsoft\VisualStudio.0\DialogPage
其中12.0
表示Visual Studio的版本。在这个键下你会发现一堆子键,它们的名字是 DialogPage
组件类型的全名。每个键包含 属性 个值;在您的情况下,您应该找到一个名为 MyOption
的 REG_SZ
值,其数据值为 True
或 False
。
My assumption is that this is due to the fact that the code is executed from different processes
除非您非常明确地编写其他内容,否则所有 VS 可扩展性代码 运行 都在 devenv.exe 下的单个 AppDomain 中。如果您看到它显示为空,则表示您的注册码没有 运行,或者没有按照您的预期进行。
这里有几个选项:
- 完全避免该问题:尝试重构您的代码,使非 VSIX 项目中的代码不必从环境中读取内容,而是将选项传递给某些方法调用,而 VSIX 项目会执行此操作.这通常是最好的结果,因为它使单元测试变得容易得多。 ("Global state" 总是不利于测试。)
- 让您的非 VSIX 项目只有一个用于设置选项的静态 class,并且您的 VSIX 项目引用非 VSIX 项目并在设置更改时设置它。
- 通过ProvideService属性注册一个服务,在你的非VSIX项目中仍然为你自己的服务类型调用
Package.GetGlobalService
。这有一个主要的警告,即 GetGlobalService 有很多问题;由于涉及的挑战,我通常避免使用该方法。 - 使用 MEF。如果您熟悉 Import/Export 属性,您可以让 VSIX 项目导出在非 VSIX 项目中定义的接口,然后导入它。如果您熟悉依赖注入的概念,这很好,但学习曲线确实是一个学习悬崖。
这个答案是对我第一个答案的补充。当然,从间谍注册表项中读取选项可能是一种肮脏的方法,因为专有 API 的实现将来可能会发生变化,这可能会破坏您的扩展。正如 Jason 提到的,您可以尝试 "avoid the problem entirely";例如,通过削减 reads/writes 选项 from/to 注册表的功能。
DialogPage
class 提供了方法 LoadSettingsFromStorage
和 SaveSettingsToStorage
,它们都是虚拟的,因此您可以覆盖它们并调用您的自定义持久性功能。
public class OptionPageGrid : DialogPage
{
private readonly ISettingsService<OptionPageGridSettings> settingsService = ...
protected override IWin32Window Window
{
get
{
return new OptionPageGridWindow(this.settingsService);
}
}
public override void LoadSettingsFromStorage()
{
this.settingsService.LoadSettingsFromStorage();
}
public override void SaveSettingsToStorage()
{
this.settingsService.SaveSettingsToStorage();
}
}
ISettingsService<T>
接口的实现可以放入一个共享程序集中,供 VSIX 项目和您的独立应用程序(您的测试适配器程序集)引用。设置服务还可以使用 Windows 注册表,或任何其他合适的存储...
public class WindowsRegistrySettingsService<T> : ISettingsService<T>
{
...
}
public interface ISettingsService<T>
{
T LoadSettingsFromStorage();
void SaveSettingsToStorage();
}
VS 测试适配器框架恰好有 API 用于跨进程共享设置。由于根本没有记录任何内容,因此花了一段时间才弄清楚如何使用 API。有关工作示例,请参阅 this GitHub project.
更新:vstest 框架最近由 MS 开源。