Skip to content

Commit c40a0d9

Browse files
Merge pull request #5 from VsixCommunity/auto-register-commands
Auto register commands
2 parents 9cde775 + e753de6 commit c40a0d9

16 files changed

+280
-27
lines changed

Community.VisualStudio.Toolkit.DependencyInjection.sln

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
88
appveyor.yml = appveyor.yml
99
src\Directory.Build.props = src\Directory.Build.props
1010
src\Directory.Build.targets = src\Directory.Build.targets
11+
nuget.config = nuget.config
1112
README.md = README.md
1213
EndProjectSection
1314
EndProject
@@ -62,10 +63,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6672CAAA
6263
EndProjectSection
6364
EndProject
6465
Global
65-
GlobalSection(SharedMSBuildProjectFiles) = preSolution
66-
src\Microsoft\Shared\Community.VisualStudio.Toolkit.DependencyInjection.Microsoft.Shared.projitems*{249f45ff-16e4-417b-99fa-686d20ed42c2}*SharedItemsImports = 13
67-
src\Core\Shared\Community.VisualStudio.Toolkit.DependencyInjection.Core.Shared.projitems*{beb996a4-e7da-4d44-a21c-493aaf842203}*SharedItemsImports = 13
68-
EndGlobalSection
6966
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7067
Debug|Any CPU = Debug|Any CPU
7168
Release|Any CPU = Release|Any CPU

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ public sealed class TestExtensionPackage : MicrosoftDIToolkitPackage<TestExtensi
2525
{
2626
// Register your services here
2727
services.AddSingleton<IYourService, YourService>();
28+
29+
// Register any commands. They can be registered as a 'Singleton' or 'Scoped'.
30+
// 'Transient' will work but in practice it will behave the same as 'Scoped'.
31+
services.AddSingleton<YourCommand>();
32+
33+
// Alternatively, you can use the 'RegisterCommands' extension method to automatically register all commands in an assembly.
34+
services.RegisterCommands(ServiceLifetime.Singleton);
2835
...
2936
}
3037

@@ -41,6 +48,67 @@ public sealed class TestExtensionPackage : MicrosoftDIToolkitPackage<TestExtensi
4148

4249
```
4350

51+
## Commands
52+
**Note**: Your commands ***MUST*** inherit from the `BaseDICommand` in order for them to work with the dependency injection system.
53+
54+
55+
### Example Command
56+
```csharp
57+
[Command(PackageIds.MyCommand)]
58+
public class MyCommand: BaseDICommand
59+
{
60+
// This is passed in through the constructor
61+
private readonly SomeSingletonObject _singletonObject;
62+
63+
public DependencyInjectionCommand(
64+
DIToolkitPackage package,
65+
SomeSingletonObject singletonObject)
66+
: base(package)
67+
{
68+
this._singletonObject = singletonObject;
69+
}
70+
71+
protected async override Task ExecuteAsync(OleMenuCmdEventArgs e)
72+
{
73+
// Your execution logic here. This is executed in the context of a new scope.
74+
...
75+
}
76+
77+
protected override void BeforeQueryStatus(EventArgs e)
78+
{
79+
// Your query logic here. This is executed in the context of a new scope.
80+
...
81+
}
82+
}
83+
```
84+
85+
### Registering Your Commands
86+
87+
You can register your commands in the DI container just like any other service.
88+
Your commands will be instantiated at the time of invocation and the lifetime with which they were registered will be followed.
89+
For example, if a command was registered as a singleton, then the same instance will be returned every time the command is invoked.
90+
If a command is registered as `Scoped`, then a new instance will be instantiated every time the command is invoked, including when the `BeforeQueryStatus` message is called.
91+
92+
```csharp
93+
protected override void InitializeServices(IServiceCollection services)
94+
{
95+
...
96+
97+
// Register any commands. They can be registered as a 'Singleton' or 'Scoped'.
98+
// 'Transient' will work but in practice it will behave the same as 'Scoped'.
99+
services.AddSingleton<YourCommand>();
100+
101+
// Alternatively, you can use the 'RegisterCommands' extension method to automatically register all commands in an assembly.
102+
services.RegisterCommands(ServiceLifetime.Singleton);
103+
104+
...
105+
}
106+
```
107+
108+
109+
Every call to the methods on your command use their own scope.
110+
What that means is that any services retrieved from the DI container will be retrieved from that scoped container.
111+
44112
## Retrieving the `IServiceProvider` from the main VS Service Provider
45113

46114
As part of the initialization process, the DI container is registered into Visual Studio's container as well.

nuget.config

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<packageSources>
4+
<add key="nuget.org" value="https://www.nuget.org/api/v2/" />
5+
<add key="Toolkit CI" value="https://ci.appveyor.com/nuget/community-visualstudio-toolkit" />
6+
</packageSources>
7+
</configuration>

src/Core/14.0/Community.VisualStudio.Toolkit.DependencyInjection.Core.14.0.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Community.VisualStudio.Toolkit.14" Version="14.0.394" />
10+
<PackageReference Include="Community.VisualStudio.Toolkit.14" Version="14.0.414" />
1111
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="1.1.1" />
1212
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
1313
</ItemGroup>

src/Core/15.0/Community.VisualStudio.Toolkit.DependencyInjection.Core.15.0.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Community.VisualStudio.Toolkit.15" Version="15.0.394" />
10+
<PackageReference Include="Community.VisualStudio.Toolkit.15" Version="15.0.414" />
1111
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="1.1.1" />
1212
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
1313
</ItemGroup>

src/Core/16.0/Community.VisualStudio.Toolkit.DependencyInjection.Core.16.0.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Community.VisualStudio.Toolkit.16" Version="16.0.394" />
10+
<PackageReference Include="Community.VisualStudio.Toolkit.16" Version="16.0.414" />
1111
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
1212
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
1313
</ItemGroup>

src/Core/17.0/Community.VisualStudio.Toolkit.DependencyInjection.Core.17.0.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Community.VisualStudio.Toolkit.17" Version="17.0.394" />
10+
<PackageReference Include="Community.VisualStudio.Toolkit.17" Version="17.0.414" />
1111
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
1212
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
1313
</ItemGroup>

src/Core/Shared/BaseDICommand.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.VisualStudio.Shell;
3+
4+
namespace Community.VisualStudio.Toolkit.DependencyInjection.Core
5+
{
6+
/// <summary>
7+
/// Base class used for commands instantiated by the DI container.
8+
/// </summary>
9+
public abstract class BaseDICommand : BaseCommand
10+
{
11+
/// <summary>
12+
/// Constructor for the BaseDICommand
13+
/// </summary>
14+
/// <param name="package"></param>
15+
public BaseDICommand(DIToolkitPackage package)
16+
{
17+
this.Package = package;
18+
var commandWrapperType = typeof(CommandWrapper<>).MakeGenericType(this.GetType());
19+
var commandWrapper = package.ServiceProvider.GetRequiredService(commandWrapperType);
20+
var commandPropertyInfo = commandWrapperType.GetProperty(nameof(BaseCommand.Command));
21+
this.Command = (OleMenuCommand)commandPropertyInfo.GetValue(commandWrapper, null);
22+
}
23+
}
24+
}

src/Core/Shared/CommandWrapper.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System;
2+
using System.ComponentModel.Design;
3+
using System.Linq;
4+
using System.Reflection;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.VisualStudio.Shell;
7+
using Task = System.Threading.Tasks.Task;
8+
9+
namespace Community.VisualStudio.Toolkit.DependencyInjection.Core
10+
{
11+
internal class CommandWrapper<T>
12+
where T : BaseDICommand
13+
{
14+
private static readonly MethodInfo _beforeQueryStatusMethod = typeof(BaseDICommand).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).First(x => x.Name == "BeforeQueryStatus" && x.GetParameters().Count() == 2);
15+
private static readonly MethodInfo _executeMethod = typeof(BaseDICommand).GetMethod("Execute", BindingFlags.Instance | BindingFlags.NonPublic);
16+
private static readonly MethodInfo _executeAsyncMethod = typeof(BaseDICommand).GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic);
17+
18+
private readonly IServiceProvider _serviceProvider;
19+
20+
public CommandWrapper(IServiceProvider serviceProvider, AsyncPackage package)
21+
{
22+
this._serviceProvider = serviceProvider;
23+
24+
CommandAttribute? attr = (CommandAttribute)typeof(T).GetCustomAttributes(typeof(CommandAttribute), true).FirstOrDefault();
25+
26+
if (attr is null)
27+
{
28+
throw new InvalidOperationException($"No [Command(GUID, ID)] attribute was added to {typeof(T).Name}");
29+
}
30+
31+
// Use package GUID if no command set GUID has been specified
32+
Guid cmdGuid = attr.Guid == Guid.Empty ? package.GetType().GUID : attr.Guid;
33+
CommandID cmd = new(cmdGuid, attr.Id);
34+
35+
this.Command = new OleMenuCommand(this.Execute, changeHandler: null, this.BeforeQueryStatus, cmd);
36+
37+
ThreadHelper.ThrowIfNotOnUIThread();
38+
39+
IMenuCommandService commandService = serviceProvider.GetRequiredService<IMenuCommandService>();
40+
commandService.AddCommand(this.Command); // Requires main/UI thread
41+
}
42+
43+
/// <summary>
44+
/// The command object associated with the command ID (GUID/ID).
45+
/// </summary>
46+
public OleMenuCommand Command { get; }
47+
48+
protected void BeforeQueryStatus(object sender, EventArgs e)
49+
{
50+
using var scope = this._serviceProvider.CreateScope();
51+
BaseDICommand instance = (BaseDICommand)scope.ServiceProvider.GetRequiredService(typeof(T));
52+
_beforeQueryStatusMethod.Invoke(instance, new object[] { sender, e });
53+
}
54+
55+
protected void Execute(object sender, EventArgs e)
56+
{
57+
using var scope = this._serviceProvider.CreateScope();
58+
BaseDICommand instance = (BaseDICommand)scope.ServiceProvider.GetRequiredService(typeof(T));
59+
_executeMethod.Invoke(instance, new object[] { sender, e });
60+
}
61+
62+
protected async Task ExecuteAsync(OleMenuCmdEventArgs e)
63+
{
64+
using var scope = this._serviceProvider.CreateScope();
65+
BaseDICommand instance = (BaseDICommand)scope.ServiceProvider.GetRequiredService(typeof(T));
66+
Task executeAsyncTask = (Task)_executeAsyncMethod.Invoke(instance, new object[] { e });
67+
await executeAsyncTask;
68+
}
69+
}
70+
}

src/Core/Shared/Community.VisualStudio.Toolkit.DependencyInjection.Core.Shared.projitems

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
<Import_RootNamespace>Community.VisualStudio.Toolkit.DependencyInjection.Core</Import_RootNamespace>
1010
</PropertyGroup>
1111
<ItemGroup>
12+
<Compile Include="$(MSBuildThisFileDirectory)BaseDICommand.cs" />
13+
<Compile Include="$(MSBuildThisFileDirectory)CommandWrapper.cs" />
1214
<Compile Include="$(MSBuildThisFileDirectory)DIToolkitPackage.cs" />
15+
<Compile Include="$(MSBuildThisFileDirectory)Extensions.cs" />
1316
<Compile Include="$(MSBuildThisFileDirectory)IToolkitServiceProvider.cs" />
1417
<Compile Include="$(MSBuildThisFileDirectory)SToolkitServiceProvider.cs" />
1518
<Compile Include="$(MSBuildThisFileDirectory)ToolkitServiceProvider.cs" />

0 commit comments

Comments
 (0)