How to merge several Office add-in projects into one binary
You know, I love working for Add-in Express! I think there are few companies that cover so many interesting things to write about. Take this article for example, Eugene asked me to write an article on how the Add-in Express team merged a number of add-ins into one assembly when they build the AbleBits Excel Add-ins Collection.
When I first heard about this topic, I had no idea how to even tackle this problem. Fortunately, one of the perks of writing this blog for Add-in Express is access to the best MS Office developers in the world, and Aleksey send me a sample project. Phew!
The scenario
Before we have a look at the sample project, let’s first examine the scenario for when you might want to consider using this approach. In the case of the Ultimate Suite for Excel, loading was a crucial factor, by following this approach the team effectively reduced the loading time by nearly 60%, because only one assembly needs to be loaded instead of 20+ separate ones.
Now if you ask me, a 60% speed increase is definitely worth-it! In this example, we’ll have two separate Microsoft Excel add-ins. One will have a custom Ribbon Tab including a Ribbon group with a few buttons and a custom Excel Task pane.
The other add-in will have a custom Ribbon Tab, custom Task Pane as well as some keyboard shortcuts and an Excel App Events component.
Before we can merge our Excel add-ins we need to create another plug-in which we’ll use to combine all the elements of the other add-ins.
Start by adding a new ADX COM Add-in project to the solution. Because the other two add-ins only support Excel, you need to select Excel as the supported application. Once the project has been created create two folders in the project called Addin1 and Addin2
Right-click on the Addin1 folder and select Add > Existing Item… Browse to the Addin1 project folder and select AddinModule.cs and the task pane class, in this example it is called ADXExcelTaskPane1.cs, and click the arrow on the right hand side of the Add button and select Add As Link.
Repeat the same process for the second plugin, only this time links the files in the Addin2 folder. Once done, the Visual Studio Solution Explorer should look similar to the following image:
Merging the add-in user interfaces
Since we’re merging our Excel add-ins we have to combine things like Ribbons tabs into a single Ribbon Tab and command bars into a single command bar. To do this, you need to add an empty ADXRibbonTab and ADXCommandBar control to the MergedAddin project’s AddinModule designer surface.
Once the required components are in place, switch to the AddinModule‘s code view and add a new List<T> object to the class:
private List<ADXAddinModule> _modules = new List<ADXAddinModule>();
We’ll add all the modules we need to merge to this collection and loop through each module’s components and add it to the MergedAddin project’s components.
public AddinModule() { Application.EnableVisualStyles(); InitializeComponent(); if (Process.GetCurrentProcess().ProcessName.ToLower() == "devenv") return; _modules.Add(new Addin1.AddinModule()); _modules.Add(new Addin2.AddinModule()); for (int i = 0; i < _modules.Count; i++) { System.ComponentModel.IContainer moduleComponents = _modules[i].GetContainer(); for (int j = moduleComponents.Components.Count - 1; j >= 0; j--) { // Keyboard Shortcuts if (moduleComponents.Components[j] is AddinExpress.MSO.ADXKeyboardShortcut) { this.components.Add(moduleComponents.Components[j]); continue; } // Excel Task Panes if (moduleComponents.Components[j] is AddinExpress.XL.ADXExcelTaskPanesCollectionItem) { this.adxExcelTaskPanesManager1.Items.Add(moduleComponents.Components[j] as AddinExpress.XL.ADXExcelTaskPanesCollectionItem); this.components.Add(moduleComponents.Components[j]); continue; } // Ribbon controls if (moduleComponents.Components[j] is IADXRibbonComponent) { if (moduleComponents.Components[j] is ADXRibbonTab) continue; if (moduleComponents.Components[j] is ADXRibbonGroup) this.adxRibbonTab1.Controls.Add(moduleComponents.Components[j] as ADXRibbonGroup); this.components.Add(moduleComponents.Components[j]); continue; } // Excel events if (moduleComponents.Components[j] is ADXExcelAppEvents) { BindEvents(moduleComponents.Components[j], this.adxExcelEvents); continue; } // Task Panes if (moduleComponents.Components[j] is AddinExpress.XL.ADXExcelTaskPanesManager) { BindEvents(moduleComponents.Components[j], this.adxExcelTaskPanesManager1); continue; } } // Module events BindEvents(_modules[i], this); } }
The BindEvents method does exactly as its name suggests and will bind the events of the source component to the destinations component in the MergedAddin project.
private void BindEvents(object source, object destination) { System.Reflection.EventInfo[] events = source.GetType().GetEvents(); if (events == null) return; for (int i = 0; i < events.Length; i++) { System.Reflection.EventInfo eInfo = events[i]; System.Reflection.FieldInfo fieldSource = source.GetType().GetField(eInfo.Name, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (fieldSource == null) // addin module (!!!) { fieldSource = source.GetType().BaseType.GetField(eInfo.Name, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); } if (fieldSource != null) { Delegate delegateSource = fieldSource.GetValue(source) as Delegate; if (delegateSource != null) { System.Reflection.FieldInfo fieldDestination = destination.GetType().GetField(eInfo.Name, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (fieldDestination == null) { fieldDestination = destination.GetType().BaseType.GetField(eInfo.Name, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); } if (fieldDestination != null) { System.Reflection.EventInfo eInfoDestination = destination.GetType().GetEvent(eInfo.Name); if (eInfoDestination != null) { eInfoDestination.AddEventHandler(destination, delegateSource); } } } } } }
If you build, register and run the MergedAddin project, you might see some strange behaviour as some of the components are added twice. To resolve this, you need to open the AddinModules of both Addin1 and Addin2 projects and add the following above the class name:
[ComVisible(false)]
Now, when you run your project you should see the UI elements of the two Excel add-ins combined into one.
Merged add-ins in Excel 2010:
There you have it! With a little bit of the Add-in Express teams’ know-how and some innovation you do not need to rewrite your add-ins to combine them into a single binary.
Thank you for reading. Until next time, keep coding!
Available downloads:
This sample Excel Add-in was developed using Add-in Express for Office and .net:
14 Comments
Hi Pieter,
thanks for the article, I am also looking to merge modules but with word and this has proved very interesting.
I do have a question though, if I wished to also use the parent project for common functionalities how would I go about it ?
Normally, I would add a refernece to the parent project and a using to access these methods but in this case with the links, VS sees double. I think that the combination of the link and the reference create a ciruclar reference.
Can you advise for this ? What is the difference between a link and a reference ? Could I implement this fonctionality only using references ? (this I will test)
Thanks
c
Hi Clodagh
To share common functionality between your parent project and for example other projects in your solution, you would ideally have to strip out the common methods etc. and place them in a separate project. This way you can add a reference to it from the parent project and any other project in the solution and avoid any circular references.
It almost sound to me that you want to separate certain functionality in your project? Maybe try using a COM Add-in Additional Module… Have a look at the “Creating modular Office add-ins using Add-in Express” article, it should shed some light.
Hope this helps. Good luck!
Hi Pieter,
Thanks for the advice, I completely agree with the stripping for a common library, it seems the best way for the common methods etc.
And thanks for the link, the modular project is exactly what I am looking for, I will implement that today and see how I go, the key part or me being that I can have my separate modules but also that they share a ribbon/command bar.
Cheers!
Hi all,
I have an error with InitializeComponent when i merge two addin with task pane.
It seems like a protection level error.
I’ve tried to add a namespace for my addin 1 (namespace : addin1) and for my addin 2 (namespace : addin2) with no results.
Help welcome.
Thanks.
Martin
Hi Martin,
Can you share some more detail about the error you get, please?
hi Pietr,
In the end I tried both of your suggestions and the method in this article proved the better choice for us.
Although, I am now having the issue that if I click a button in one of my added modules, the CurrentInstance and the hostApplication for this module returns null.
I don’t quite understand how an onClick() can trigger but the WordApp be null.. and in only one of the two added modules… any suggestions would be greatly appreciated. i’m a bit confused :(
thx
c
Hi Pieter,
I wanted to know how to merge 2 addins (addin1 and addin2).
Both of them contain a single taskpane(and have the same name), an addinmodule with a taskpanemanager.
I have “namespaced” my two addinmodules and taskpanes
Exemple :
ADXPowerPointTaskPane1.vb
“(…) Namespace MyAddin1
Public Class ADXPowerPointTaskPane1
Inherits AddinExpress.PP.ADXPowerPointTaskPane
Public Sub New()
MyBase.New()
InitializeComponent()(…)”
AddinModule.vb
“(…) Namespace MyAddin1
‘Add-in Express Add-in Module
_
_
Public Class AddinModule
Inherits AddinExpress.MSO.ADXAddinModule
Friend WithEvents AdxPowerPointTaskPanesCollectionItem1 As AddinExpress.PP.ADXPowerPointTaskPanesCollectionItem
Friend WithEvents AdxPowerPointTaskPanesManager1 As AddinExpress.PP.ADXPowerPointTaskPanesManager
Public Sub New()
MyBase.New() (…)”
My error :
InitializeComponent() in both Addinmodule is unaccessible (of course, it’s declared as private).
Can you help me with this please ? :)
Hi Clodagh,
Can you send a sample add-in that causes the same problem to the Add-in Express support email, please?
It’ll make finding the problem easier.
Hi Martin,
Can you send the project that gives you the error to our support mail? Will be easier to find the problem that way.
Hello!
Let’s say Addin1 has a ribbon button with an onclick event that calls an “ExcelApp.Calculate()”… In this scenario I am getting an exception because HostApplication is null for Addin1.
The same happens if I have any reference to ExcelApp in Addin2, HostApplication -and therefore ExcelApp- is null.
However, MergeAddin’s HostApplication/ExcelApp works. But, how do I reference it in any of the “sub-addins”?
I have used my project and the sample project provided with this article and I have this same issue in both.
Hi Adriana,
Mmm…not sure on why you’re seeing this problem. Would you mind sending your test add-in to out support e-mail?
It’ll be easier to track down the problem if I see your code.
Thank you!
Hello Adriana,
Thank you for sending the project.
To fix the issue with ExcelApp returning null (Nothing in VB.NET) , you need to use conditional compiling. Say, you can define a conditional variable called Merged, you can define the CurrentInstance and ExcelApp properties of the original modules as follows:
#if Merged
public static new MergedAddin.AddinModule CurrentInstance {
get {
return AddinExpress.MSO.ADXAddinModule.CurrentInstance as MergedAddin.AddinModule;
}
}
public Excel._Application ExcelApp {
get {
return MergedAddin.AddinModule.CurrentInstance.ExcelApp;
}
}
#else
public static new AddinModule CurrentInstance {
get {
return AddinExpress.MSO.ADXAddinModule.CurrentInstance as AddinModule;
}
}
public Excel._Application ExcelApp {
get {
return (HostApplication as Excel._Application);
}
}
#endif
That is, when the module above (Addin1.AddinModule or Addin1.AddinModule) is used in the merged add-in, the ExcelApp property is taken from the ExcelApp property of MergedAddin.AddinModule. The same applies to CurrentInstance. But. Those buts… There’s a problem with AddinExpress.MSO.ADXAddinModule.CurrentInstance: creating a new add-in module instance makes ADXAddinModule.CurrentInstance return this instance. To avoid this, you add the following code line to the constructor of the merged add-in module:
ADXAddinModule.CurrentInstance = this;
In VB.NET:
ADXAddinModule.CurrentInstance = Me
What is the reason behind having Addin1 and Addin2 added to MergedAddin as a link?
Why not added as a reference in MergedAddin to the Addin1 and addin2 projects? Why would this not work?
Hi Adriana,
It’s because the source code of 2 add-ins (Addin1 and Addin2) is required for MergedAddin to be built. Adding only references won’t work, since we take both source codes and combine them in the new add-in.