Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - Tool window injection #14

Open
tonyhallett opened this issue Oct 17, 2023 · 3 comments
Open

Feature - Tool window injection #14

tonyhallett opened this issue Oct 17, 2023 · 3 comments

Comments

@tonyhallett
Copy link

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 #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
Copy link
Author

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

@tonyhallett
Copy link
Author

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

        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

protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)

            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
Copy link
Author

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


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant