A Complete Shared Add-in Template

Over the years I have worked with a number of customers that still use/require the use of Shared Add-ins in Office. One customer I am working with today frequently has questions about implementations or encounters scenarios that are specific to Shared Add-ins.

If you have been working with Visual Studio for some time, you know there used to be a Shared Add-in template you could use to create an add-in. However, as of Visual Studio 2012, that template is now gone. Additionally, you might have found it was difficult to figure out how to implement Ribbons and TaskPanes.

With this in mind, I decided to publish on CodePlex a shared add-in template I have created for myself and that I think might be useful for others. The link is here:

Complete Shared Add-in C# Template

How to Use It

Once you load this solution, you will find the Connect.cs. This is the MAIN entry point for the DLL solution. The first thing you will want to do is define which applications this add-in will pertain to. This is done via the ThisAddin_Register() method. There is an object called “office” where you will define each application you want to hook to:

office.AddApplication(OfficeBase.APPTYPE.EXCEL, “Excel Shared Addin Example”, “Excel Shared Addin Example”);
office.AddApplication(OfficeBase.APPTYPE.WORD, “Word Shared Addin Example”, “Word Shared Addin Example”);
office.AddApplication(OfficeBase.APPTYPE.POWERPOINT, “PowerPoint Shared Addin Example”, “PowerPoint Shared Addin Example”);
office.AddApplication(OfficeBase.APPTYPE.OUTLOOK, “Outlook Shared Addin Example”, “Outlook Shared Addin Example”);

The AddApplication command will allow you to specify which application will load. And if you look in the IDTExtensibility2 code region you will find the RegisterFunction method. This method will setup/install this DLL (when registered via RegAsm), to work with COM Automation and also register it in each application that you setup in the ThisAddin_Register() method:

foreach(OfficeBase.OfficeAddinInfo info in office.GetAddins()) {}

This template already comes with Ribbons for each of the applications, Word, Excel, PowerPoint and Outlook. It also automatically loads each from the Resources.

NOTE: Outlook requires a bit more work than what is provided.

If you look at my previous post on how to create a TaskPane in a Shared Com Add-in, you will see that this has been incorporated in this sample as well. To add a task pane, you will define your control and then call like this:

SharedCustomTaskPane myPane = CustomTaskPaneCollection.Add(dynamicControl, “My Pane”);

You will also want to change the attributes at the top of the class:

[GuidAttribute(“11111111-AF27-4814-9CBE-ED6A39A4B9A5”),
ProgId(“SharedAddin.Connect”),
ClassInterface(ClassInterfaceType.AutoDual)]

When the add-in loads in a parent application, it will determine the type and then setup the “office” object so that it contains the reference to the parent application. So to get Excel, you can make a simple call like this:

Excel.Application xlApp = office.GetExcel();

Likewise, there is a similar function for Word, PowerPoint, and Outlook. From there you can attach to events and make additional calls as needed.

Hopefully, you will find this solution useful. It has been very useful to me over the years, especially as I have added to it. There are still many uses for this type of Add-in and having all your code in one place – across all the Office applications can be very useful.

Do not use the VSTO ListObject

The Microsoft.Office.Tools.Excel library has some quite handy extensions for Excel. However, one of them is far more trouble than it is worth. The VSTO ListObject promises to provide extended support for lists and named ranges, including events and additional properties and methods. However, there are a number of issues that make this class flawed:

  1. Deleting rows has always been concerning and quite problematic. Such as deleting the last row, deleting the first row, deleting one of three selected rows, and deleting the last row when there are only two rows. This will either break the list or give you unexpected results, such as not notifying you of a delete or being unable to determine the changes made.
  2. Copy/Paste has some serious problems. One issue (https://support.microsoft.com/en-us/kb/3081715), was fixed. However, you will still have similar issues as related to deleting, especially if data is pasted over existing rows.
  3. Finally, filtering a ListObject LO and then trying to determine changes, especially if the user deletes or pastes data into a range. It will break your solution.

The VSTO ListObject seemingly helps a lot, especially when you are looking for a little extra to extend yor solution. However, it is best ignored. This advice also comes directly from the Visual Studio Tools for Officeproduct team. In one Program Managers words, “there be dragons there.” You have been warned. 😐

The advice is to use the native ListObject in Microsoft.Office.Interop.Excel and work within the limitations there, for being limited is far better than being broken. 😷

Office Add-ins (Apps for Office) and window.alert()

What! So, you are getting started writing an Apps for Office or Office add-in as they are now known, and you need to display a JavaScript alert. Maybe you are doing this because you need to prompt the user with some information, or maybe you are doing this because you need to test something. So you enter your code:

window.alert("Hello world!");

When you run it, nothing happens.

This is because JavaScript allows you to redefine anything. And the Office team redefined certain window functions because they felt they were intrusive and because they do not work the same (or at all) on some platforms. So the suggestion is to not use them. The proper way to do this is to use the app.ShowNotifification command:

app.showNotification(title, text)

However, if you are like me and use them to help you debug/code/proof things, you might really need them. I found that if you add the following to your Office.initialize method, they will work:

// The initialize function must be run each time a new page is loaded
Office.initialize = function (reason) {
    $(document).ready(function () {
        app.initialize();

        delete window.alert;       // assures alert works
        delete window.confirm;     // assures confirm works
        delete window.prompt;      // assures prompt works
        
        /* the rest of your code here */
}}

Detect Shape Selection in Excel

A problem I have encountered  from time to time is how to detect whether or not there is a shape selected in Excel. The problem is that the Excel object model does not fire the WindowsSelecitonChange event if and when the user clicks on a shape. So one of the workarounds out there today is to hook to the CommandBars.OnUpdae event and then look to see if one f the commands associated with Shapes is enabled, like ShapeFillColorPicker. The problem I have found is that this method worked in Excel 2007 and Excel 2010, but it stopped working or began working intermittently for me in Excel 2013. So, I had to develop a bit of a workaround to this problem – which also works still in Excel 2010 and Excel 2007 and to the best I can tell causes no impact to performance in Excel. I start a thread, have it sleep for a short time, hook and then unhook the OnUpdate event and then DoEvents for good measure. Like this:

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
new Thread(() =>
{
while (true)
{
Thread.Sleep(25);
Application.CommandBars.OnUpdate -= CommandBars_OnUpdate;
Application.CommandBars.OnUpdate += CommandBars_OnUpdate;
System.Windows.Forms.Application.DoEvents();
}
}).Start();
}

void CommandBars_OnUpdate()
{
try
{
if (Application.CommandBars.GetEnabledMso("ShapeFillColorPicker"))
{
// A Shape was selected
}
}
catch { }
}

How to Insert Raw RTF into Excel

This is a question I have had a few times in the past with respect to Word, Excel and PowerPoint. I will focus on Excel for this example, but the basics apply just the same for any other application.

From time to time certain applications may generate reports or data in RTF or you may have a need to store RAW RTF into a database text field. There are a lot of advantages to RTF in this respect since it is a fully formatted document format, but it can be hard to get into an Office application without it looking… RAW. Disappointed smile

This is where this little snippet of code comes in handy: Hot smile

Excel.Application LobjXL = Globals.ThisAddIn.Application;
// get the current data on the clipboard
IDataObject LobjClipboardContents = Clipboard.GetDataObject();
// extract the text/rtf
string LstrText = "" // <-- PUT YOUR RTF SOURCE HERE
// create an RTF control on the fly and use it
RichTextBox LobjRtfControl = new RichTextBox();
LobjRtfControl.Rtf = LstrText;
LobjRtfControl.SelectAll();
LobjRtfControl.Copy(); // copy the layout to the clipboard as RTF
// and paste
Excel.Worksheet LobjSheet = LobjXL.ActiveSheet;
LobjSheet.Paste();
// then reset the clipboard to the original format
Clipboard.SetDataObject(LobjClipboardContents);

Office wide After Save As Event (and tangent on extension methods and lambdas in Office code)

First off, I am way behind on my blogging. I actually owe a few blog entries to some folks that I will be getting around to. Life and work has been busy, complicated, not quite as balanced as I would like. Disappointed smile But this one issue has recently come up and is directly customer focused, therefore it gets the priority.

I was recently asked how to handle an After Save As scenario exactly the same in each application – as closely as possible. And only the Save As scenario. This is the scenario in which the user s saving a document for the first time or the user is choosing to same the same file with a different name and/or location.

So, wild tangent time… Hot smile  If you have been following my blogs for a while you will find out two things I like to do, call them programming style:

  1. Use extension methods
  2. Use inline Lambda expressions

This example today is no different. However, I recently got into a philosophical discussion on why I take these two approaches and WHY I think why you should too.

<rant>
Extension methods allow you to encapsulate a lot of code, allow for multiple re-use in other projects and once tested and vetted, keep the root entry points of your code (usually event methods or ribbon button clicks) cleaner. They are not any harder to debug, but do allow a debugger to potentially step over a large operation with an F10. I am very much about clean and neat code.

Lambda expressions are – lets admit it – cool. Smile But they serve a purpose, especially in threads to make “flow” more obvious. This is sort of the opposite end of the extension method argument in that sometimes putting a smaller operational block into the same method from which it will only ever be derived/called, just makes sense. It keeps it all in one place and easier to follow/debug.

Anyway, these are my opinions and different developers have their own styles. I invariably lose the conversation each time with my customers because by the time I get to their code: “it is just not the way we do things here.” Oh well. Punch
</rant>

With that said, lets get down to business. I have created a class called OfficeExtentions that works best in a separate Windows DLL project that you then reference in your core VSTO project. I demonstrate how to call it later. But here is the class:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using PowerPoint = Microsoft.Office.Interop.PowerPoint;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word;

namespace OfficeExtensions
{
public static class OfficeExtensions
{
public delegate void AfterPowerPointSaveHandler(PowerPoint.Presentation PobjPres);
public delegate void AfterWordSaveHandler(Word.Document PobjDoc);
public delegate void AfterExcelSaveHandler(Excel.Workbook PobjWb);
private const int MCintDELAY = 1000;

/// <summary>
/// POWERPOINT EXTENSION METHOD - AFTER SAVE
/// This function allows you to pass in a function that you want to
/// have called after PowerPoint has completed a SaveAs. If the user
/// only performs a save, your method will not be called. This only
/// gets called when the Save As Dialog is used.
/// </summary>
/// <param name="PobjApp"></param>
/// <param name="PobjFunc"></param>
public static void AttachToPowerPointAfterSaveAsEvent(this PowerPoint.Application PobjApp, AfterPowerPointSaveHandler PobjFunc)
{
// FIRST - we attach to the BeforeSave event
PobjApp.PresentationBeforeSave += (PowerPoint.Presentation PobjPres, ref bool PbolCancel) =>
{
// start a new thread using LAMBDA
new Thread(() =>
{
Thread.Sleep(MCintDELAY); // need this delay
if (hasSaveAsDialogOpen())
{
while (hasSaveAsDialogOpen())
{
Thread.Sleep(1);
System.Windows.Forms.Application.DoEvents();
}
// look for the saveAs dialog and as long as it
// is open we will wait right here
PobjFunc.Invoke(PobjPres);
}
}
).Start();
};
}

/// <summary>
/// WORD EXTENSION METHOD - AFTER SAVE
/// This function allows you to pass in a function that you want to
/// have called after Word has completed a SaveAs. If the user
/// only performs a save, your method will not be called. This only
/// gets called when the Save As Dialog is used.
/// </summary>
/// <param name="PobjApp"></param>
/// <param name="PobjFunc"></param>
public static void AttachToWordAfterSaveAsEvent(this Word.Application PobjApp, AfterWordSaveHandler PobjFunc)
{
// FIRST - we attach to the BeforeSave event
PobjApp.DocumentBeforeSave += (Word.Document PobjDoc, ref bool PbolSaveAsUi, ref bool PbolCancel) =>
{
// start a new thread using LAMBDA
new Thread(() =>
{
Thread.Sleep(MCintDELAY); // need this delay
if (hasSaveAsDialogOpen())
{
while (hasSaveAsDialogOpen())
{
Thread.Sleep(1);
System.Windows.Forms.Application.DoEvents();
}
// look for the saveAs dialog and as long as it
// is open we will wait right here
PobjFunc.Invoke(PobjDoc);
}
}
).Start();
};
}

/// <summary>
/// EXCEL EXTENSION METHOD - AFTER SAVE
/// This function allows you to pass in a function that you want to
/// have called after Excel has completed a SaveAs. If the user
/// only performs a save, your method will not be called. This only
/// gets called when the Save As Dialog is used.
/// </summary>
/// <param name="PobjApp"></param>
/// <param name="PobjFunc"></param>
public static void AttachToExcelAfterSaveAsEvent(this Excel.Application PobjApp, AfterExcelSaveHandler PobjFunc)
{
// FIRST - we attach to the BeforeSave event
PobjApp.WorkbookBeforeSave += (Excel.Workbook PobjWb, bool PbolSaveAsUi, ref bool PbolCancel) =>
{
// start a new thread using LAMBDA
new Thread(() =>
{
Thread.Sleep(MCintDELAY); // need this delay
if (hasSaveAsDialogOpen())
{
while (hasSaveAsDialogOpen())
{
Thread.Sleep(1);
System.Windows.Forms.Application.DoEvents();
}
// look for the saveAs dialog and as long as it
// is open we will wait right here
PobjFunc.Invoke(PobjWb);
}
}
).Start();
};
}

#region API CODE
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string windowTitle);

[DllImport("user32.dll", SetLastError = true)]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

/// <summary>
/// Helper function to see if Word has any dialogs open
/// </summary>
/// <returns></returns>
private static bool hasSaveAsDialogOpen()
{
const string LCstrWIN_CLASS = "#32770";
const string LCstrWIN_CAPTION = "Save As";
IntPtr LintHWin = IntPtr.Zero;
LintHWin = FindWindowEx(IntPtr.Zero, LintHWin, LCstrWIN_CLASS, LCstrWIN_CAPTION);
uint PID = 0;
while (LintHWin != IntPtr.Zero)
{
// Make sure that the window handle that we got is for the current running
// Office Application process. We do this by checking if the PID for this window
// our Office application are the same.
GetWindowThreadProcessId(LintHWin, out PID);
if (PID == Process.GetCurrentProcess().Id)
break; // found it and it belongs to our app
// get next window
LintHWin = FindWindowEx(IntPtr.Zero, LintHWin, LCstrWIN_CLASS, LCstrWIN_CAPTION);
}
return LintHWin != IntPtr.Zero;
}
#endregion
}
}

What this is doing is giving an extension method off the root of Application that allows you to attach to the event. When the event occurs it calls your function parameter.

I have built three extensions methods:

  • AttachToExcelAfterSaveEvent
  • AttachToWordAfterSaveEvent
  • AttachToPowerPointAfterSaveEvent

NOTE: I tried to get creative and simply create one overloaded function called “AttachToAfterSaveAsEvent” but this failed to compile in the Excel and PowerPoint VSTO add-ins because they required the Word.Application to be defined. Fair enough – if I added Word.Application references to my Excel and PowerPoint VSTO project all was copasetic – but WHY. Seems there is some strangeness going on in the Interops when using overloaded functions. I did not have time to investigate this further, so rather than require you to reference all the apps in each of your separate application specific projects, I opted for different names. If you feel so inclined to get it to work… please let me know if you do and you figured it out.

Each of these methods works the same. They attach to the BeforeSave event in each application, and kick off a thread. The thread is used so we will know we are OUTSIDE of the event handler when they are executed. In the thread we issue a small delay to allow the dialog to appear, and then look for a Save As dialog. If we detect one we go into a loop looking for that Save As dialog to disappear. Once it does – we call the function you define/passed as a parameter.

Here are the different ways you can call it:

  • As a LAMBA expression:
/// <summary>
/// STARTUP
/// </summary>
/// <param name="PobjSender"></param>
/// <param name="PobjEventArgs"></param>
private void ThisAddIn_Startup(object PobjSender, System.EventArgs PobjEventArgs)
{
// Attach a method to the Extension methods After Before Save As event
// in this case we are doing LAMBDA expression
Application.AttachToPowerPointAfterSaveAsEvent((PowerPoint.Presentation PobjPres) =>
{
MessageBox.Show("The filename is: " + PobjPres.FullName);
});
}

  • Or traditional means:
/// <summary>
/// STARTUP
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
// Attach a method to the Extension methods After Before Save As event
Application.AttachToExcelAfterSaveAsEvent(HandleAfterBeforeSaveAs);
}

/// <summary>
/// Handles the after SaveAs dialog
/// </summary>
/// <param name="PobjPres"></param>
private void HandleAfterBeforeSaveAs(Excel.Workbook PobjWb)
{
MessageBox.Show("The filename is: " + PobjWb.FullName);
}

There it is.

Also, I recently added RATINGS to my posts. Please rate this post if you so feel inspired to. I would like to know it is being read and that you found it useful. Open-mouthed smile

Counting down…

With one month and one week to go, it is time to start moving off Windows XP and Office 2003. Here is another great article from Microsoft about how/why:

Support for Windows XP and Office 2003 ends April 8, 2014 — what’s next?
http://blogs.technet.com/b/firehose/archive/2014/02/26/support-for-windows-xp-and-office-2003-ends-april-8-2014-what-s-next.aspx

A few interesting highlights from the article:

  • Windows XP and Office 2003, however, have been supported for more than a decade, or since “Baywatch” went off the air.
  • Computers currently running Windows XP and Office 2003 won’t stop working on April 9, but over time security and performance will be affected: Many newer apps won’t run on Windows XP; new hardware may not support Windows XP; and without critical security updates, PCs may become vulnerable to harmful viruses, spyware and other malicious software that can steal or damage personal information and business data.
  • Office 365 — the next generation of familiar Office productivity applications in the cloud. The subscription-based service offers familiar Office tools and maintains file integrity and design when documents are edited by multiple people, and it provides enterprise-class security and privacy.

If you are considering the move and have questions about your Microsoft Office Integrated Line of Business Applications, there are many ways Microsoft and Microsoft partners can assist you in assessing and remediating these solutions.

You can learn more about Office 365 for your business here: http://blogs.office.com/office365forbusiness/

CodePlex: Loading an Excel UDF from VSTO

When I look at my stats on my blog, one post gets dozens of hits a day:

[UPDATE] Creating Excel UDF’s in C#
http://davecra.com/2013/06/08/update-creating-excel-udfs-in-c/

This post is very popular and looking on the web, I see a lot of people chattering about it and a lot of questions. So, I have been asked repeatedly if I can post a project that demonstrates this, rather than just supply the code. I finally got around to creating a project on CodePlex and posted the source there. Here is a link to the shared project:

Loading an Excel UDF from VSTO

Getting Appointments on a specific date from Outlook

I was working on proof of concept Outlook add-in for a customer when I ran into  series of distressing exceptions trying to access specific recurring appointments from the calendar. There seems to be a great many ways to get a list of appointments for a specific date, but you may find the .Start date of these vary wildly. I went down a path using GetRecurrencePattern().GetOccurrence(DateTime.Now), and I got this exception a LOT:

“You changed one of the recurrences of this item, and this instance no longer exists. Close any open items and try again.”

After doing some more searching, I found that I was going about it all wrong. Now, I will be the first to say that I am not the strongest in the Outlook Object Model. Excel, Word and especially PowerPoint are by bread and butter. But sometimes, Outlook can be just downright confusing. Disappointed smile

In the end, this is what I came up with to get a list of all the appointments on a given date in a specific users calendar that you select from the Address list:

Outlook.Recipient LobjRecipient = null;
// crete a select names dialog
Outlook.SelectNamesDialog LobjSnd = MobjOutlook.Session.GetSelectNamesDialog();
// limit to the TO box
LobjSnd.NumberOfRecipientSelectors = Outlook.OlRecipientSelectors.olShowTo;
LobjSnd.AllowMultipleSelection = false; // there can be only one
LobjSnd.Display(); // display it
// do we have resolved names
if (!LobjSnd.Recipients.ResolveAll())
{
LobjRecipient = null; // NO
return; // exit out
}
else
{
LobjRecipient = LobjSnd.Recipients[1]; // yes
}
LobjSnd = null;
// open the shares Calendar folder
Outlook.MAPIFolder LobjFolder = MobjOutlook.ActiveExplorer().Session.GetSharedDefaultFolder(
LobjRecipient, Outlook.OlDefaultFolders.olFolderCalendar)
as Outlook.MAPIFolder;
// get all the items
Outlook.Items LobjItems = LobjFolder.Items;
LobjItems.Sort("[Start]"); // sort the items by start date
LobjItems.IncludeRecurrences = true; // be sure to include recurrences
string LstrDay = DateTime.Now.ToShortDateString(); // today
// set the find string to today 0:00 to 23:59:59
string LstrFind = "[Start] <= \"" + LstrDay + " 11:59 PM\"" +
" AND [End] > \"" + LstrDay + " 12:00 AM\"";
// find the first appointment for the day
Outlook.AppointmentItem LobjAppt = LobjItems.Find(LstrFind);
while (LobjAppt != null)
{
// ...do your thing here...

// get the next item
LobjAppt = LobjItems.FindNext();
}

XP/2003 Deadline Looms

If you are still on Windows XP and Office 2003, if you have not already started your migration, you should start ASAP. The end of support for BOTH is this April. Here is a great link that explains all the reasons you should start today: http://www.microsoft.com/en-us/windows/enterprise/endofsupport.aspx.

I have heard from a number of folks that are “stuck” in XP/2003 land. Namely, because they have a large number of Office based solutions, Excel VBA add-ins, XLL’s, UDF’s, macros and Access 2003 databases that must be migrated and no idea how to begin to remediate them. There is help out there and a number of partners and even service offerings from Microsoft that can help you.

If you have a solution in Office 2003 that you need help to remediate, please contact me. Send me a private tweet or send me an InMail on LinkedIn.