Skip to content

Commit e10cf98

Browse files
Added the ability to register a command in the DI container. This also takes care of registering the command with Visual Studio.
1 parent 69cc117 commit e10cf98

8 files changed

+207
-21
lines changed

Community.VisualStudio.Toolkit.DependencyInjection.sln

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6672CAAA
6262
EndProjectSection
6363
EndProject
6464
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
6965
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7066
Debug|Any CPU = Debug|Any CPU
7167
Release|Any CPU = Release|Any CPU

src/Core/Shared/BaseDICommand.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Reflection;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.VisualStudio.Shell;
5+
using Task = System.Threading.Tasks.Task;
6+
7+
namespace Community.VisualStudio.Toolkit.DependencyInjection.Core
8+
{
9+
/// <summary>
10+
/// Base class used for commands instantiated by the DI container.
11+
/// </summary>
12+
public abstract class BaseDICommand
13+
{
14+
private static readonly PropertyInfo _commandPropertyInfo = typeof(CommandWrapper<>).GetProperty(nameof(Command));
15+
16+
/// <summary>
17+
/// The package.
18+
/// </summary>
19+
public DIToolkitPackage Package { get; }
20+
21+
/// <summary>
22+
/// The command object associated with the command ID (GUID/ID).
23+
/// </summary>
24+
public OleMenuCommand Command { get; }
25+
26+
/// <summary>
27+
/// Constructor
28+
/// </summary>
29+
/// <param name="package"></param>
30+
public BaseDICommand(DIToolkitPackage package)
31+
{
32+
this.Package = package;
33+
var commandWrapperType = typeof(CommandWrapper<>).MakeGenericType(this.GetType());
34+
var commandWrapper = package.ServiceProvider.GetRequiredService(commandWrapperType);
35+
PropertyInfo commandPropertyInfo = commandWrapperType.GetProperty(nameof(Command));
36+
this.Command = (OleMenuCommand)commandPropertyInfo.GetValue(commandWrapper, null);
37+
}
38+
39+
/// <summary>
40+
/// Execute the command logica
41+
/// </summary>
42+
/// <param name="sender"></param>
43+
/// <param name="e"></param>
44+
protected internal virtual void Execute(object sender, EventArgs e)
45+
{
46+
Package.JoinableTaskFactory.RunAsync(async delegate
47+
{
48+
try
49+
{
50+
await ExecuteAsync((OleMenuCmdEventArgs)e);
51+
}
52+
catch (Exception ex)
53+
{
54+
await ex.LogAsync();
55+
}
56+
}).FireAndForget();
57+
}
58+
59+
/// <summary>Executes asynchronously when the command is invoked and <see cref="Execute(object, EventArgs)"/> isn't overridden.</summary>
60+
/// <remarks>Use this method instead of <see cref="Execute"/> if you're invoking any async tasks by using async/await patterns.</remarks>
61+
protected internal virtual Task ExecuteAsync(OleMenuCmdEventArgs e)
62+
{
63+
return Task.CompletedTask;
64+
}
65+
66+
/// <summary>
67+
/// BeforeQueryStatus
68+
/// </summary>
69+
/// <param name="sender"></param>
70+
/// <param name="e"></param>
71+
protected internal virtual void BeforeQueryStatus(object sender, EventArgs e)
72+
{
73+
BeforeQueryStatus(e);
74+
}
75+
76+
/// <summary>Override this method to control the commands visibility and other properties.</summary>
77+
protected virtual void BeforeQueryStatus(EventArgs e)
78+
{
79+
// Leave empty
80+
}
81+
}
82+
}

src/Core/Shared/CommandWrapper.cs

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

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
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" />
1315
<Compile Include="$(MSBuildThisFileDirectory)IToolkitServiceProvider.cs" />
1416
<Compile Include="$(MSBuildThisFileDirectory)SToolkitServiceProvider.cs" />

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
99
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
1010
<PropertyGroup />
11+
<ItemGroup>
12+
<Compile Include="BaseDICommand.cs" />
13+
<Compile Include="CommandWrapper.cs" />
14+
</ItemGroup>
1115
<Import Project="Community.VisualStudio.Toolkit.DependencyInjection.Core.Shared.projitems" Label="Shared" />
1216
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
13-
</Project>
17+
</Project>

src/Core/Shared/DIToolkitPackage.cs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using Microsoft.VisualStudio.Shell;
55
using Task = System.Threading.Tasks.Task;
66
using Community.VisualStudio.Toolkit.DependencyInjection.Core;
7+
using System.Linq;
8+
using System.ComponentModel.Design;
79

810
namespace Community.VisualStudio.Toolkit.DependencyInjection
911
{
@@ -12,14 +14,9 @@ namespace Community.VisualStudio.Toolkit.DependencyInjection
1214
/// </summary>
1315
/// <typeparam name="TPackage"></typeparam>
1416
[ProvideService(typeof(SToolkitServiceProvider<>), IsAsyncQueryable = true)]
15-
public abstract class DIToolkitPackage<TPackage> : ToolkitPackage
17+
public abstract class DIToolkitPackage<TPackage> : DIToolkitPackage
1618
where TPackage : AsyncPackage
1719
{
18-
/// <summary>
19-
/// Custom ServiceProvider for the package.
20-
/// </summary>
21-
public IToolkitServiceProvider<TPackage> ServiceProvider { get; private set; } = null!; // This property is initialized in `InitializeAsync`, so it's never actually null.
22-
2320
/// <summary>
2421
/// Initializes the <see cref="AsyncPackage"/>
2522
/// </summary>
@@ -31,14 +28,17 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
3128
await base.InitializeAsync(cancellationToken, progress);
3229

3330
IServiceCollection services = this.CreateServiceCollection();
31+
services.AddSingleton<DIToolkitPackage>(this); // Add the 'DIToolkitPackage' to the services
3432
services.AddSingleton<AsyncPackage>(this); // Add the 'AsyncPackage' to the services
3533
services.AddSingleton(this.GetType(), this); // Add the exact package type to the services as well.
34+
services.AddSingleton((IMenuCommandService)await this.GetServiceAsync(typeof(IMenuCommandService)));
35+
services.AddSingleton(typeof(CommandWrapper<>));
3636

3737
// Initialize any services that the implementor desires.
3838
InitializeServices(services);
3939

4040
IServiceProvider serviceProvider = BuildServiceProvider(services);
41-
ServiceProvider = new ToolkitServiceProvider<TPackage>(serviceProvider);
41+
this.ServiceProvider = new ToolkitServiceProvider<TPackage>(serviceProvider);
4242

4343
// Add the IToolkitServiceProvider to the VS IServiceProvider
4444
AsyncServiceCreatorCallback serviceCreatorCallback = (sc, ct, t) =>
@@ -47,7 +47,32 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
4747
};
4848

4949
AddService(typeof(SToolkitServiceProvider<TPackage>), serviceCreatorCallback, true);
50+
51+
// Register any commands that were added to the DI container
52+
// Create a CommandWrapper for each command that was added to the container
53+
var commands = services
54+
.Where(x => typeof(BaseDICommand).IsAssignableFrom(x.ImplementationType))
55+
.ToList();
56+
57+
foreach (var command in commands)
58+
{
59+
var baseCommandTypeGeneric = typeof(CommandWrapper<>).MakeGenericType(command.ImplementationType);
60+
61+
// Retrieveing the command wrapper from the container will register the command with the 'IMenuCommandService'
62+
_ = serviceProvider.GetRequiredService(baseCommandTypeGeneric);
63+
}
5064
}
65+
}
66+
67+
/// <summary>
68+
/// Package that contains a DI service container.
69+
/// </summary>
70+
public abstract class DIToolkitPackage : ToolkitPackage
71+
{
72+
/// <summary>
73+
/// Custom ServiceProvider for the package.
74+
/// </summary>
75+
public IServiceProvider ServiceProvider { get; protected set; } = null!; // This property is initialized in `InitializeAsync`, so it's never actually null.
5176

5277
/// <summary>
5378
/// Create the service collection.

test/VSSDK.TestExtension/Commands/DependencyInjectionCommand.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
using System.Threading.Tasks;
22
using Community.VisualStudio.Toolkit;
3+
using Community.VisualStudio.Toolkit.DependencyInjection;
34
using Community.VisualStudio.Toolkit.DependencyInjection.Core;
45
using Microsoft.VisualStudio.Shell;
56
using VSSDK.TestExtension;
67

78
namespace TestExtension.Commands
89
{
910
[Command(PackageIds.TestDependencyInjection)]
10-
internal sealed class DependencyInjectionCommand : BaseCommand<DependencyInjectionCommand>
11+
internal sealed class DependencyInjectionCommand : BaseDICommand
1112
{
12-
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
13+
private readonly SomeSingletonObject _singletonObject;
14+
public DependencyInjectionCommand(DIToolkitPackage package, SomeSingletonObject singletonObject)
15+
: base(package)
16+
{
17+
this._singletonObject = singletonObject;
18+
}
19+
20+
protected async override Task ExecuteAsync(OleMenuCmdEventArgs e)
1321
{
1422
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
1523

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
using System;
22
using System.Runtime.InteropServices;
3-
using System.Threading;
43
using Community.VisualStudio.Toolkit.DependencyInjection.Microsoft;
4+
using Microsoft.Extensions.DependencyInjection;
55
using Microsoft.VisualStudio.Shell;
66
using TestExtension;
77
using TestExtension.Commands;
8-
using Task = System.Threading.Tasks.Task;
98

109
namespace VSSDK.TestExtension
1110
{
@@ -14,14 +13,20 @@ namespace VSSDK.TestExtension
1413
[ProvideMenuResource("Menus.ctmenu", 1)]
1514
public sealed class TestExtensionPackage : MicrosoftDIToolkitPackage<TestExtensionPackage>
1615
{
17-
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
16+
protected override void InitializeServices(IServiceCollection services)
1817
{
19-
await base.InitializeAsync(cancellationToken, progress);
18+
base.InitializeServices(services);
2019

21-
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
20+
// Register any commands into the DI container
21+
services.AddTransient<DependencyInjectionCommand>();
2222

23-
// Commands
24-
await DependencyInjectionCommand.InitializeAsync(this);
23+
// Register anything else
24+
services.AddSingleton<SomeSingletonObject>();
2525
}
2626
}
27+
28+
internal class SomeSingletonObject
29+
{
30+
31+
}
2732
}

0 commit comments

Comments
 (0)