3
votes

I'm currently developing my first VS extension, which needs to provide some options to the user. Following https://msdn.microsoft.com/en-us/library/bb166195.aspx, it was rather easy to come up with my own options page. However, I have not yet found out how to read my options.

The solution structure of my extension is as follows:

MySolution
  MyProject (generates a DLL from C# code)
  MyProjectVSIX

Following the tutorial cited above, I have added a VS Package to my VSIX project and configured it as described. As a result, my options page with my options is shown under Tools/Options. Nice! Here's my DialogPage implementation:

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; }
    }
}

And here's the head of my Package 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
{
    ...

However, I now want to read those options, and I want to do it from MyProject (which does not have a dependency on MyProjectVSIX). And this is where I'm kind of lost. My first attempt was to let my Package implement an IOptions interface, and to let it register itself by calling a static method Options.Register(IOptions) from the Package's constructor. This works (i.e., the breakpoint in Register() is hit), but when I try to read options, the static IOptions instance is still null. My assumption is that this is due to the fact that the code is executed from different processes (which is beyond my control).

After some more googling, I tried to get an instance of the DTE object (which would allow me to read my options if I correctly understood), but to no success. I've tried several variants, including the one described at https://msdn.microsoft.com/en-us/library/ee834473.aspx and

DTE Dte = Package.GetGlobalService(typeof(DTE)) as DTE;

I always end up with a null reference.

Finally, since the tutorial suggested to access the options through the instance of Package, I tried to figure out how to obtain such an instance of my VS Package through some kind of registry (which I could than cast to IOptions), but again without luck.

Can anybody please point me to the right direction? Or is it even impossible to access VS options from a non-VSIX project?

Update: I have done some more research, and one piece of information was missing: My extension is a Unit Test Adapter. This seems to imply that the test discovery code as well as the test execution code are run from different processes, i.e., my assumption was right.

I have in the meantime managed to access the DTE object of the VS instance I'm running in (I will post that with my complete solution as soon as my problem is solved), but still have problems accessing the options. In fact, the following code (copied from here: https://msdn.microsoft.com/en-us/library/ms165641.aspx) works nicely:

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

However, if I change the above as follows:

Properties txtEdCS = DTEProvider.DTE.get_Properties(CATEGORY_NAME, PAGE_NAME);

Now the code crashes. Weirdly enough, I can see my property category and page in the registry under HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\14.0Exp_Config\AutomationProperties\My Test Adapter\General. Searching for my properties shows them under HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\14.0Exp\ApplicationPrivateSettings\MyProjectVSIX\OptionPageGrid (mabye because I added

OptionPageGrid Page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));
Page.SaveSettingsToStorage();

to the Package's Initialize() method (as Matze suggested), maybe because I haven't looked there before :-) ).

So how to read my properties?

4

4 Answers

2
votes

If you want to read the options from within your package, you can just ask for an OptionPageGrid instance via the GetDialogPage method of the VSPackage. For instance:

var options = (OptionGridPage)this.package.GetDialogPage(typeof(OptionGridPage));
bool b = options.MyOption;

In case you want access the options from within another application (or an assembly which can be used without having the runtime environment of Visual Studio), you can try to read those settings right from the Windows registry, but the registry keys and values may not be present unless the options have been written by the IDE or your package. You can force persistence of the options by calling the SaveSettingsToStorage method from within your package, for instance on the first load:

options.SaveSettingsToStorage();

The settings will be stored under the following key:

HKEY_CURRENT_USER\SOFTWARE\Microsoft\VisualStudio\12.0\DialogPage

whereby 12.0 indicates the version of Visual Studio. Under this key you´ll find a bunch of sub-keys whose names are the full names of the DialogPage component types. Each key contains the property values; in your case you should find a REG_SZ value named MyOption having either True or False as it´s data value.

1
votes

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

Unless you're very explicitly writing stuff otherwise, all VS extensibility code runs in a single AppDomain under devenv.exe. If you're seeing it come up null, that means either your registration code didn't run, or didn't do what you thought it did.

You have a few options here:

  1. Avoid the problem entirely: try refactoring your code such that the code in the non-VSIX project doesn't have to read stuff from the environment, but is passed options into some method call, and the VSIX project does that. This is often the best outcome, because it makes unit testing a lot easier. ("Global state" is always bad for testing.)
  2. Have your non-VSIX project just have a static class for setting the options, and your VSIX project references the non-VSIX project and sets it when the settings get changed.
  3. Register a service through the ProvideService attribute, and in your non-VSIX project still call Package.GetGlobalService for your own service type. This has the major caveat that GetGlobalService has a bunch of gotchas; I avoid that method in general due to the challenges involved with it.
  4. Use MEF. If you are familiar with Import/Export attributes, you can have your VSIX project Export an interface defined in your non-VSIX project, and you import it. If you're familiar with the idea of dependency injection this is very nice, but the learning curve is really a learning cliff.
1
votes

This answer is an addition to my first answer. Of course, reading options from a spied registry key might be a dirty approach, because the implementation of the proprietary API could change in the future, which could break your extension. As Jason mentioned, you could try to "avoid the problem entirely"; for instance by cutting the functionality that reads/writes the options from/to the registry.

The DialogPage class provides the methods LoadSettingsFromStorage and SaveSettingsToStorage which are both virtual, so you can override them and call your custom persistence functionality instead.

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

The implementation of the ISettingsService<T> interface can be put into a shared assembly, to be referenced by the VSIX project and your stand-alone application (your test adapter assembly). The settings service could also use the Windows registry, or any other suitable storage...

public class WindowsRegistrySettingsService<T> : ISettingsService<T> 
{
    ...
}

public interface ISettingsService<T>
{
    T LoadSettingsFromStorage();
    void SaveSettingsToStorage();
}
1
votes

The VS test adapter framework happens to have API for sharing settings accross processes. Since nothing is documented at all, it took a while to figure out how to use that API. For a working example see this GitHub project.

Update: The vstest framework has recently been open-sourced by MS.