VsixCommunity / Community.VisualStudio.Toolkit.DependencyInjection

Adds Dependency Injection tooling for the Community.VisualStudio.Toolkit
Other
13 stars 6 forks source link

Feature - Tool window injection #14

Open tonyhallett opened 9 months ago

tonyhallett commented 9 months ago

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#L76

Which 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.

public interface IToolWindowProvider
{
    Type PaneType { get; }

    string GetTitle(int toolWindowId);

    Task<FrameworkElement> CreateAsync(int toolWindowId, CancellationToken cancellationToken);
}

    public abstract class BaseDIToolWindowRegistration<T, TToolWindowProvider> : BaseToolWindow<T> where T : BaseToolWindow<T>, new() where TToolWindowProvider : IToolWindowProvider
    {
        private readonly TToolWindowProvider toolWindowProvider;

        public BaseDIToolWindowRegistration()
        {
            static Type GetToolkitPackageType()
            {
                // StackTrace / Assembly.GetCallingAssembly / Attribute / Source generator
            }
            var toolkitPackageType = GetToolkitPackageType();

// see https://github.com/VsixCommunity/Community.VisualStudio.Toolkit.DependencyInjection/issues/13 for 
// SToolkitServiceProviderContainer
// a static ServiceProvider property would be better

#pragma warning disable VSTHRD104 // Offer async methods
            var serviceProvider = ThreadHelper.JoinableTaskFactory.Run(async () => {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
                var toolkitServiceProviderContainer = await VS.GetRequiredServiceAsync<SToolkitServiceProviderContainer, IToolkitServiceProviderContainer>();
                return toolkitServiceProviderContainer.Get(toolkitPackageType);
            });
#pragma warning restore VSTHRD104 // Offer async methods
            toolWindowProvider = (TToolWindowProvider)serviceProvider.GetRequiredService(typeof(TToolWindowProvider));
        }

        public override Type PaneType => toolWindowProvider.PaneType;

        public override Task<FrameworkElement> CreateAsync(int toolWindowId, CancellationToken cancellationToken)
        {
            return toolWindowProvider.CreateAsync(toolWindowId, cancellationToken);
        }

        public override string GetTitle(int toolWindowId)
        {
            return toolWindowProvider.GetTitle(toolWindowId);
        }
    }

    public static class Extensions
    {
       // AS BEFORE
        public static IServiceCollection RegisterCommands(this IServiceCollection services, ServiceLifetime serviceLifetime, params Assembly[] assemblies){}

        private static readonly Type _registrationType = typeof(BaseDIToolWindowRegistration<,>);
        private static Type? GetToolWindowProviderType(Type derivedType)
        {
            if (derivedType == null) return null;

            var baseType = derivedType.BaseType;
            while (baseType != null)
            {
                if (baseType.IsGenericType)
                {
                    var genericTypeDefinition = baseType.GetGenericTypeDefinition();
                    if (genericTypeDefinition == _registrationType)
                    {
                        return baseType.GenericTypeArguments[1];
                    }
                }
                baseType = baseType.BaseType;
            }
            return null;
        }

        public static IServiceCollection RegisterToolWindows(this IServiceCollection services, params Assembly[] assemblies)
        {
            if (!(assemblies?.Any() ?? false))
                assemblies = new Assembly[] { Assembly.GetCallingAssembly() };
            foreach (var assembly in assemblies)
            {
                var toolWindowProviderTypes = assembly.GetTypes().Select(t => GetToolWindowProviderType(t)).Where(t => t != null);

                foreach (var toolWindowProviderType in toolWindowProviderTypes)
                    services.Add(new ServiceDescriptor(toolWindowProviderType, toolWindowProviderType, ServiceLifetime.Singleton));
            }
            return services;

        }
    }

Usage

    public sealed class TestDIPackage : MicrosoftDIToolkitPackage<TestDIPackage>
    {
        protected override void InitializeServices(IServiceCollection services)
        {
            services.RegisterCommands(ServiceLifetime.Singleton);
            services.RegisterToolWindows();
            // register dependencies
            services.AddMEF(); // extension method available on request
        }

        protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
        {
            await base.InitializeAsync(cancellationToken, progress);
            this.RegisterToolWindows(); // as before
        }
    }

[Export(typeof(ProxyMeffed))]
public class ProxyMeffed
{
    public string GetTitle() => "Proxy !";

}
public class ProxyToolWindowProvider : IToolWindowProvider
{
    private readonly ProxyMeffed meffed;
    public ProxyToolWindowProvider(ProxyMeffed meffed)
    {
        this.meffed = meffed;
    }

    public Type PaneType => typeof(Pane);

    public async System.Threading.Tasks.Task<FrameworkElement> CreateAsync(int toolWindowId, CancellationToken cancellationToken)
    {
        await Task.Delay(0);
        return new ToolWindowControl();
    }

    public string GetTitle(int toolWindowId)
    {
        return meffed.GetTitle();
    }

    [Guid("82563071-D155-4ED3-AD14-CC434AC00D29")]
    internal class Pane : ToolWindowPane
    {
        public Pane()
        {
            // Set an image icon for the tool window
            BitmapImageMoniker = KnownMonikers.StatusInformation;
        }
    }

}

public class ProxyToolWindow : BaseDIToolWindowRegistration<ProxyToolWindow, ProxyToolWindowProvider> { }

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.

tonyhallett commented 9 months ago

Usage tested against local nuget feed built from fork https://github.com/tonyhallett/Community.VisualStudio.Toolkit.DependencyInjection

tonyhallett commented 9 months ago

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

    }

}

Add to https://github.com/VsixCommunity/Community.VisualStudio.Toolkit.DependencyInjection/blob/639e8340345a0d528670b70f7817af662d64c01a/src/Core/Shared/Extensions.cs#L11

        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);
            }
tonyhallett commented 9 months ago

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