There have been a couple of blog posts about delay loading VSTO addins, and with Dev10 being released soon, it’s a good time to expand on the subject a little bit.

This has been covered before, and I’ll avoid spending too much time here on topics that have already been covered.  For an overview of delay loading, please read these posts.

http://blogs.msdn.com/andreww/archive/2008/07/14/demand-loading-vsto-add-ins.aspx

http://blogs.msdn.com/andreww/archive/2008/04/19/delay-loading-the-clr-in-office-add-ins.aspx

There are many reasons why you would want to delay load your addin, the most important of which is for performance – loading the CLR can easily double the amount of time that it takes to load an Office application. If your addin is only used occasionally, your users will appreciate the faster load times.

One important improvement that has gone into Dev10 is that you no longer need to manually change the LoadBehavior in the registry for your addin to make it DelayLoad. If you go into project properties of your addin, click on the “Publish” tab, and go into “Options…”, you can adjust the Add-in Load Behavior under “Office Settings”.

clip_image002

For more information about publishing, please see Publishing VSTO Solutions.

Now, once you have done this, your addin will not be loaded until Office decides that it needs to load the addin. This can happen a number of ways, but the most common is that a user action (like clicking on a ribbon button) calls into the addin, and causes the addin to be loaded.

One important note is that any custom “get” handler (getEnabled, getVisible, getLabel) will not trigger the loading of your addin. The first time that your addin is loaded, Office will call the handler and cache the response given, and use that response as the default.  It will then call the handler again to use in the current session.  This is important because if the control is disabled initially, and then later enabled via a call to RibbonUI.Invalidate or RibbonUI.InvalidateControl, it will only invalidate this second value and not the first value. 

Consider the following example where a database connection is necessary for the addin to function:

XML:

<group id="MyGroup"
       label="Database controls"  >
    <button id="Button1" label="Get Data" onAction="GetDataClick" getEnabled="GetEnabled" />
</group>

Callbacks in Ribbon Class:

#region Ribbon Callbacks
Boolean dbEnabled = false; 
public void Ribbon_Load(Office.IRibbonUI ribbonUI)
{
    this.ribbon = ribbonUI;

    //[aDeveloper] moving the database connection to another thread
    Thread databaseConnectionThread = 
        new Thread(delegate()
        { 
            Thread.Sleep(500); // todo:  Actually connect
            dbEnabled = true;
            ribbon.Invalidate();
        });
    databaseConnectionThread.Start();
}

public Boolean GetEnabled(Office.IRibbonControl rc)
{
    return dbEnabled;
}

public void GetDataClick(Office.IRibbonControl rc)
{
    //pull data from the database and populate the spreadsheet with it.
}
#endregion

This can cause a chicken and egg type situation if by default none of its ribbon controls are available (Ie, they’re not enabled or visible). The next time Office is started, it will use the cached “false” response resulting in none of the controls being enabled, and the user will have no easy way to load the addin and cause the “get” handlers to be called.

The best way around this issue would be to have a control that is always enabled that calls into the addin and thus would load the addin. You can then call ribbonUI.Invalidate in the custom UI’s onLoad handler to force the Office application to call each “get” handler.

XML:

<group id="MyGroup"
       label="Database controls">
    <button id="Button0" label="Connect Database" onAction="ConnectDatabase" enabled="true" />
    <button id="Button1" label="Get Data" onAction="GetDataClick" getEnabled="GetEnabled" />
</group>

Callbacks in Ribbon Class:

#region Ribbon Callbacks
Boolean dbEnabled = false; 
public void Ribbon_Load(Office.IRibbonUI ribbonUI)
{
    this.ribbon = ribbonUI;
}

public Boolean GetEnabled(Office.IRibbonControl rc)
{
    return dbEnabled;
}

public void GetDataClick(Office.IRibbonControl rc)
{
    //pull data from the database and populate the spreadsheet with it.
}

public void ConnectDatabase(Office.IRibbonControl rc)
{
    //[aDeveloper] we don't do this automatically since we don't want to connect to the database
    //             unless the user wants to - there's a chance for a hang if the database isn't
    //             available.
    Thread.Sleep(500); // todo:  Actually connect
    dbEnabled = true;
    ribbon.Invalidate();
}
#endregion

Another trick is to have the handler return “true” the first time that it’s called, so that Office caches the “true” response as the default, and the control is enabled.  If you do this, you’ll want to make sure that your click handlers can handle the case where the button should be disabled.

(n.b.:  This example is a little bit contrived, but will illustrate what you need to do if your application would be best served by this behavior.) 

In this case, the “Get Data” button will be enabled by default.  The first click will generate the message box since by the time that GetDataClick is called, the addin hasn’t had time to ‘connect’, and the button will remain disabled until the connection is done.

XML:

<group id="MyGroup"
       label="Database controls">
    <button id="Button1" label="Get Data" onAction="GetDataClick" getEnabled="GetEnabled" />
</group>

Callbacks in Ribbon Class:

#region Ribbon Callbacks
Boolean dbEnabled = false;
Boolean firstCall = true;
public void Ribbon_Load(Office.IRibbonUI ribbonUI)
{
    this.ribbon = ribbonUI;
    //[aDeveloper] moving the database connection to another thread so that we don't hang office
    Thread databaseConnectionThread = 
        new Thread(delegate()
        { 
            Thread.Sleep(5000); // todo:  Actually connect
            dbEnabled = true;
            ribbon.Invalidate();
        });
    databaseConnectionThread.Start();
}

public Boolean GetEnabled(Office.IRibbonControl rc)
{
    if (true == firstCall)
    {
        firstCall = false;
        return true;
    }
    return dbEnabled;
}

public void GetDataClick(Office.IRibbonControl rc)
{
    if (false == dbEnabled)
    {
        System.Windows.Forms.MessageBox.Show("Could not connect to database");
                
        //Disable this button until we get the connection up.
        firstCall = false;
        ribbon.Invalidate();
        return;
    }
    //Get the data and populate the spreadsheet
}
#endregion