Open tonyhallett opened 9 months ago
Usage tested against local nuget feed built from fork https://github.com/tonyhallett/Community.VisualStudio.Toolkit.DependencyInjection
Alternative
public abstract class BaseDiToolWindow
{
private static BaseDiToolWindow implementation;
public BaseDiToolWindow(DIToolkitPackage diToolkitPackage)
{
implementation = this;
DiBaseToolWindowProxy.Initialize(diToolkitPackage);
}
public static Task ShowAsync(int id = 0, bool create = true)
{
return DiBaseToolWindowProxy.ShowAsync(id, create);
}
public static Task HideAsync(int id = 0)
{
return DiBaseToolWindowProxy.HideAsync(id);
}
/// <summary>
/// Gets the title to show in the tool window.
/// </summary>
/// <param name="toolWindowId">The ID of the tool window for a multi-instance tool window.</param>
protected abstract string GetTitle(int toolWindowId);
/// <summary>
/// Gets the type of <see cref="ToolWindowPane"/> that will be created for this tool window.
/// </summary>
protected abstract Type PaneType { get; }
/// <summary>
/// Creates the UI element that will be shown in the tool window.
/// Use this method to create the user control or any other UI element that you want to show in the tool window.
/// </summary>
/// <param name="toolWindowId">The ID of the tool window instance being created for a multi-instance tool window.</param>
/// <param name="cancellationToken">The cancellation token to use when performing asynchronous operations.</param>
/// <returns>The UI element to show in the tool window.</returns>
protected abstract Task<FrameworkElement> CreateAsync(int toolWindowId, CancellationToken cancellationToken);
/// <summary>
/// Called when the <see cref="ToolWindowPane"/> has been initialized and "sited".
/// The pane's service provider can be used from this point onwards.
/// </summary>
/// <param name="pane">The tool window pane that was created.</param>
/// <param name="toolWindowId">The ID of the tool window that the pane belongs to.</param>
protected virtual void SetPane(ToolWindowPane pane, int toolWindowId)
{
// Consumers can override this if they need access to the pane.
}
private class DiBaseToolWindowProxy : BaseToolWindow<DiBaseToolWindowProxy>
{
internal BaseDiToolWindow implementation;
public DiBaseToolWindowProxy()
{
implementation = BaseDiToolWindow.implementation;
}
public override string GetTitle(int toolWindowId) => implementation.GetTitle(toolWindowId);
public override Task<FrameworkElement> CreateAsync(int toolWindowId, CancellationToken cancellationToken)
{
return implementation.CreateAsync(toolWindowId, cancellationToken);
}
public override Type PaneType => implementation.PaneType;
public override void SetPane(ToolWindowPane pane, int toolWindowId)
{
implementation.SetPane(pane, toolWindowId);
}
}
}
public static IServiceCollection RegisterToolWindows(this IServiceCollection services, ServiceLifetime serviceLifetime, params Assembly[] assemblies)
{
if (!(assemblies?.Any() ?? false))
assemblies = new Assembly[] { Assembly.GetCallingAssembly() };
foreach (var assembly in assemblies)
{
var toolWindowTypes = assembly.GetTypes().Where(t => typeof(BaseDiToolWindow).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var toolWindowType in toolWindowTypes)
{
services.Add(new ServiceDescriptor(toolWindowType, toolWindowType, serviceLifetime));
}
}
return services;
}
In InitializeAsync https://github.com/VsixCommunity/Community.VisualStudio.Toolkit.DependencyInjection/blob/639e8340345a0d528670b70f7817af662d64c01a/src/Core/Shared/DIToolkitPackage.cs#L26
var baseDiToolWindowsSds = services.Where(sd => typeof(BaseDiToolWindow).IsAssignableFrom(sd.ServiceType));
// after switching to the main thread
foreach(var baseDiToolWindowSd in baseDiToolWindowsSds)
{
serviceProvider.GetRequiredService(baseDiToolWindowSd.ServiceType);
}
Reflection is another, lesser, alternative
internal static class PackageReflectionMethods
{
public static readonly MethodInfo AddToolWindow = typeof(ToolkitPackage).GetMethod("AddToolWindow", BindingFlags.Instance | BindingFlags.NonPublic);
}
public static class BaseDIToolWindowType
{
public static bool IsBaseDIToolWindowType(Type derivedType)
{
if (derivedType == null) return false;
var baseType = derivedType.BaseType;
while (baseType != null)
{
if (baseType.IsGenericType)
{
var genericTypeDefinition = baseType.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(BaseDIToolWindow<>))
{
return true;
}
}
baseType = baseType.BaseType;
}
return false;
}
public static void RegisterToolWindows(this IServiceCollection services, IServiceProvider serviceProvider)
{
var diToolWindowServices = services.Where(service => IsBaseDIToolWindowType(service.ImplementationType));
foreach (var diToolWindowService in diToolWindowServices)
{
_ = serviceProvider.GetRequiredService(diToolWindowService.ImplementationType);
}
}
}
public static class ServiceRegistrationExtensions
{
public static IServiceCollection RegisterToolWindows(this IServiceCollection services, ServiceLifetime serviceLifetime, params Assembly[] assemblies)
{
if (!(assemblies?.Any() ?? false))
assemblies = new Assembly[] { Assembly.GetCallingAssembly() };
foreach (var assembly in assemblies)
{
var diToolWindowTypes = assembly.GetTypes()
.Where(x => BaseDIToolWindowType.IsBaseDIToolWindowType(x));
foreach (var diToolWindowType in diToolWindowTypes)
services.Add(new ServiceDescriptor(diToolWindowType, diToolWindowType, serviceLifetime));
}
return services;
}
}
public abstract class BaseDIToolWindow<T> : BaseToolWindow<T> where T : BaseToolWindow<T>, new()
{
private static readonly Type BaseToolWindowType = typeof(BaseToolWindow<T>);
public BaseDIToolWindow() { }
public BaseDIToolWindow(AsyncPackage package)
{
EnsureProvidesToolWindow(package);
SetPackageProperties(package);
SetStaticImplementationField();
AddToolWindow();
}
private void EnsureProvidesToolWindow(AsyncPackage package)
{
// Verify that the package has a ProvideToolWindow attribute for this tool window.
ProvideToolWindowAttribute[] toolWindowAttributes = (ProvideToolWindowAttribute[])package.GetType().GetCustomAttributes(typeof(ProvideToolWindowAttribute), true);
ProvideToolWindowAttribute foundToolWindowAttr = toolWindowAttributes.FirstOrDefault(a => a.ToolType == this.PaneType);
if (foundToolWindowAttr == null)
{
Debug.Fail($"The tool window '{this.GetType().Name}' requires a ProvideToolWindow attribute on the package."); // For testing debug build of the toolkit (not for users of the release-built nuget package).
throw new InvalidOperationException($"The tool window '{this.GetType().Name}' requires a ProvideToolWindow attribute on the package.");
}
}
private void SetPackageProperties(AsyncPackage package)
{
var packageProperty = BaseToolWindowType.GetProperty("Package", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
packageProperty.SetValue(this, package as ToolkitPackage);
var staticPackageField = BaseToolWindowType.GetField("_package", BindingFlags.Static | BindingFlags.NonPublic);
staticPackageField.SetValue(null, package as ToolkitPackage);
}
private void SetStaticImplementationField()
{
var staticImplementationField = BaseToolWindowType.GetField("_implementation", BindingFlags.Static | BindingFlags.NonPublic);
staticImplementationField.SetValue(null, this);
}
private void AddToolWindow()
{
PackageReflectionMethods.AddToolWindow.Invoke(this.Package, new object[] { this });
}
}
The Community Toolkit works as follows
Uses reflection to invoke the static Initialize method of
BaseToolWindow<T>
types. https://github.com/VsixCommunity/Community.VisualStudio.Toolkit/blob/5071b7e871e5ad3c585c858e35692f8debdb28f9/src/toolkit/Community.VisualStudio.Toolkit.Shared/ExtensionMethods/AsyncPackageExtensions.cs#L76Which adds them ( as the internal IToolWindowProvider interface ) with the internal package AddToolWindow method. https://github.com/VsixCommunity/Community.VisualStudio.Toolkit/blob/5071b7e871e5ad3c585c858e35692f8debdb28f9/src/toolkit/Community.VisualStudio.Toolkit.Shared/Windows/BaseToolWindow.cs#L70
https://github.com/VsixCommunity/Community.VisualStudio.Toolkit/blob/5071b7e871e5ad3c585c858e35692f8debdb28f9/src/toolkit/Community.VisualStudio.Toolkit.Shared/ToolkitPackage.cs#L19
You could reinvent all of the code of the base ToolkitPackage or...
Create a proxy derivation that used a public IToolWindowProvider obtained from the service provider.
Usage
We need to get to the specific service provider from the BaseDIToolWindowRegistration constructor. As mentioned in https://github.com/VsixCommunity/Community.VisualStudio.Toolkit.DependencyInjection/issues/13 the only current method, using the Vs service added DIToolkitPackage InitailizeAsync, is broken. The code above uses the quick and dirty workaround suggested in the issue.