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();

PackageInitialize() 方法(正如 Matze 所建议的),也许是因为我之前没有看过那里:-))。

那么如何读取我的属性?

如果你想从包中读取选项,你可以通过 VSPackageGetDialogPage 方法请求一个 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 组件类型的全名。每个键包含 属性 个值;在您的情况下,您应该找到一个名为 MyOptionREG_SZ 值,其数据值为 TrueFalse

My assumption is that this is due to the fact that the code is executed from different processes

除非您非常明确地编写其他内容,否则所有 VS 可扩展性代码 运行 都在 devenv.exe 下的单个 AppDomain 中。如果您看到它显示为空,则表示您的注册码没有 运行,或者没有按照您的预期进行。

这里有几个选项:

  1. 完全避免该问题:尝试重构您的代码,使非 VSIX 项目中的代码不必从环境中读取内容,而是将选项传递给某些方法调用,而 VSIX 项目会执行此操作.这通常是最好的结果,因为它使单元测试变得容易得多。 ("Global state" 总是不利于测试。)
  2. 让您的非 VSIX 项目只有一个用于设置选项的静态 class,并且您的 VSIX 项目引用非 VSIX 项目并在设置更改时设置它。
  3. 通过ProvideService属性注册一个服务,在你的非VSIX项目中仍然为你自己的服务类型调用Package.GetGlobalService。这有一个主要的警告,即 GetGlobalService 有很多问题;由于涉及的挑战,我通常避免使用该方法。
  4. 使用 MEF。如果您熟悉 Import/Export 属性,您可以让 VSIX 项目导出在非 VSIX 项目中定义的接口,然后导入它。如果您熟悉依赖注入的概念,这很好,但学习曲线确实是一个学习悬崖。

这个答案是对我第一个答案的补充。当然,从间谍注册表项中读取选项可能是一种肮脏的方法,因为专有 API 的实现将来可能会发生变化,这可能会破坏您的扩展。正如 Jason 提到的,您可以尝试 "avoid the problem entirely";例如,通过削减 reads/writes 选项 from/to 注册表的功能。

DialogPage class 提供了方法 LoadSettingsFromStorageSaveSettingsToStorage,它们都是虚拟的,因此您可以覆盖它们并调用您的自定义持久性功能。

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 开源。