4
\$\begingroup\$

Here is the new & improved Rubberduck 2.x entry point class, based on MZ-Tools 8.0's Connect.VBA implementation of the IDTExtensibility2 COM interface, graciously shared to the Rubberduck team by Mr. MZ-Tools himself, Carlos Quintero.

The modifications made are not yet merged into the project's main repository at the time of this writing, but you can view it on commit 8ac768d9 in my own fork.

I've yet to shim the add-in to get it to be a good citizen and run in its own dedicated AppDomain (.net creates RCW's per-AppDomain, that's why it does matter), so there are still a number of issues beyond just the "entry point" to completely stabilize Rubberduck 2.x, but for now I'm interested in feedback specifically on the IDTExtensibility2 implementation - a useful add-in, I'm discovering, is much trickier to "do right" than what the "hello world" tutorials imply.

Also, since we're using for dependency injection, I'm also interested in feedback on the composition root here, the handling of the _kernel instance and, given the various different ways an add-in can be loaded/unloaded, whether this (which seems to work fine) is done right.

Of course, any other feedback is welcome, too.

using Extensibility;
using Microsoft.Vbe.Interop;
using Ninject;
using Ninject.Extensions.Factory;
using Rubberduck.Root;
using Rubberduck.UI;
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Windows.Threading;
using Ninject.Extensions.Interception;
using NLog;
using Rubberduck.Settings;
using Rubberduck.SettingsProvider;

namespace Rubberduck
{
    /// <remarks>
    /// Special thanks to Carlos Quintero (MZ-Tools) for providing the general structure here.
    /// </remarks>
    [ComVisible(true)]
    [Guid(ClassId)]
    [ProgId(ProgId)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    // ReSharper disable once InconsistentNaming // note: underscore prefix hides class from COM API
    public class _Extension : IDTExtensibility2
    {
        private const string ClassId = "8D052AD8-BBD2-4C59-8DEC-F697CA1F8A66";
        private const string ProgId = "Rubberduck.Extension";

        private VBE _ide;
        private AddIn _addin;
        private bool _isInitialized;
        private bool _isBeginShutdownExecuted;

        private IKernel _kernel;
        private App _app;
        private readonly Logger _logger = LogManager.GetCurrentClassLogger();

        public void OnAddInsUpdate(ref Array custom) { }

        // ReSharper disable InconsistentNaming
        public void OnConnection(object Application, ext_ConnectMode ConnectMode, object AddInInst, ref Array custom)
        {
            try
            {
                _ide = (VBE)Application;
                _addin = (AddIn)AddInInst;
                _addin.Object = this;

                switch (ConnectMode)
                {
                    case ext_ConnectMode.ext_cm_Startup:
                        // normal execution path - don't initialize just yet, wait for OnStartupComplete to be called by the host.
                        break;
                    case ext_ConnectMode.ext_cm_AfterStartup:
                        InitializeAddIn();
                        break;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

        Assembly LoadFromSameFolder(object sender, ResolveEventArgs args)
        {
            var folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            var assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll");
            if (!File.Exists(assemblyPath))
            {
                return null;
            }

            var assembly = Assembly.LoadFile(assemblyPath);
            return assembly;
        }

        public void OnStartupComplete(ref Array custom)
        {
            InitializeAddIn();
        }

        public void OnBeginShutdown(ref Array custom)
        {
            _isBeginShutdownExecuted = true;
            ShutdownAddIn();
        }

        // ReSharper disable InconsistentNaming
        public void OnDisconnection(ext_DisconnectMode RemoveMode, ref Array custom)
        {
            switch (RemoveMode)
            {
                case ext_DisconnectMode.ext_dm_UserClosed:
                    ShutdownAddIn();
                    break;

                case ext_DisconnectMode.ext_dm_HostShutdown:
                    if (_isBeginShutdownExecuted)
                    {
                        // this is the normal case: nothing to do here, we already ran ShutdownAddIn.
                    }
                    else
                    {
                        // some hosts do not call OnBeginShutdown: this mitigates it.
                        ShutdownAddIn();
                    }
                    break;
            }
        }

        private void InitializeAddIn()
        {
            if (_isInitialized)
            {
                // The add-in is already initialized. See:
                // The strange case of the add-in initialized twice
                // http://msmvps.com/blogs/carlosq/archive/2013/02/14/the-strange-case-of-the-add-in-initialized-twice.aspx
                return;
            }

            _kernel = new StandardKernel(new NinjectSettings { LoadExtensions = true }, new FuncModule(), new DynamicProxyModule());

            try
            {
                var currentDomain = AppDomain.CurrentDomain;
                currentDomain.AssemblyResolve += LoadFromSameFolder;

                var config = new XmlPersistanceService<GeneralSettings>
                {
                    FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Rubberduck", "rubberduck.config")
                };

                var settings = config.Load(null);
                if (settings != null)
                {
                    try
                    {
                        var cultureInfo = CultureInfo.GetCultureInfo(settings.Language.Code);
                        Dispatcher.CurrentDispatcher.Thread.CurrentUICulture = cultureInfo;
                    }
                    catch (CultureNotFoundException) { }
                }

                _kernel.Load(new RubberduckModule(_ide, _addin));

                _app = _kernel.Get<App>();
                _app.Startup();
                _isInitialized = true;
            }
            catch (Exception exception)
            {
                _logger.Fatal(exception);
                System.Windows.Forms.MessageBox.Show(exception.ToString(), RubberduckUI.RubberduckLoadFailure, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void ShutdownAddIn()
        {
            if (_app != null)
            {
                _app.Shutdown();
                _app = null;
            }

            if (_kernel != null)
            {
                _kernel.Dispose();
                _kernel = null;
            }

            Marshal.ReleaseComObject(_addin);
            Marshal.ReleaseComObject(_ide);
            _isInitialized = false;
        }
    }
}
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

The code that handles the methods OnConnection, OnStartupComplete, OnBeginShutdown and OnDisconnection and their flags is correct. I cannot comment on root composition or NInject.

Some things worth documenting in the code of why it must be that way, just in case some day someone thinks that the code can be "simplified":

  • If you call InitializeAddIn in the OnConnection method when ConnectMode is ext_ConnectMode.ext_cm_Startup, some hosts (VB5) may not have initialized yet the MainWindow property, or the MainWindow.hWnd property. So, the OnStartupComplete method must be used.

  • If you call ShutdownAddIn in the OnDisconnection method when RemoveMode is ext_DisconnectMode.ext_dm_HostShutdown, some hosts crash. So, the OnBeginShutdown method must be used. This has a small side effect: Office prompts to save dirty documents after OnBeginShutdown is called. If the user cancels in the prompt to save dialog, the host is not closed but the add-in was unloaded. It is a small price to pay.

There is only one VB6 host and only one VB5 host, but there are tons of VBA hosts: not only Office versions (2000, 2002, 2003, 2007, 2010, 2013 and 2016) but also other 3rd party hosts that provide VBA:

The code above is robust enough to handle all those in my experience.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.