Share via


NoReplyAll Lite

So far, all of the Office add-in work I've been talking about here has been based on VSTO, but some users of NoReplyAll have complained about having to install the .NET Framework (or, in some cases, install whatever version I'd chosen to use, because they've standardised on an earlier one, and don't want to, or can't, upgrade to newer). Additionally, there have been a couple of occasions where the difference between .NET's (garbage collected) way of managing resources and COM's deterministic mechanism has caused problems - for example, Outlook windows that won't go away, or an inability to delete .msg files - my solution has been to add a bunch of ugly Marshal.ReleaseComObject calls, or even to force a garbage collection, which really takes me away from the convenience and elegance of VSTO. Because of these, it's been at the back of my mind to try writing a native C++ add-in, just to see how difficult it actually is - and I've finally got round to it.

Now, there are examples of native add-ins for Outlook out there - here are a couple of articles worth reading  - the examples I've come across have, like these, used ATL. I find that ATL is very useful for creating COM objects of any complexity but feel that it's overkill for simple COM objects that have no Windows UI of their own: I don't like dragging in extra code to initialise things I don't need and it is a pain trying to debug through layers of wrappers (wrappers that were very convenient when writing the code, it must be said). Years ago, I user to work with DirectShow, and the COM framework that appeared in associated samples used was much simpler than ATL - through reading those and some of the shell integration samples, I've ended up writing a lot of COM material without using any support framework.

Given all that, there are two parts to the add-in: "boilerplate" COM registration, and talking to Outlook. A very good summary of the first can be found in Raymond Chen's blog and I won't repeat it here but will focus on the second. My Outlook add-in needs to implement three COM interfaces: IDTExtensibility2 (for an Office application to be able to load it), IRibbonExtensibility (as the name suggests, to be able to interact with the ribbon), and IDispatch (to handle ribbon callbacks); and the first of those is pretty straightforward:

   STDMETHOD(OnConnection)(LPDISPATCH /*application*/, AddinDesign::ext_ConnectMode /*connectMode*/, LPDISPATCH /*addInInst*/, SAFEARRAY** /*custom*/)
 {
       return S_OK;
    }
   STDMETHOD(OnDisconnection)(AddinDesign::ext_DisconnectMode /*removeMode*/, SAFEARRAY** /*custom*/)
  {
       return S_OK;
    }
   STDMETHOD(OnAddInsUpdate)(SAFEARRAY** /*custom*/)
   {
       return S_OK;
    }
   STDMETHOD(OnStartupComplete)(SAFEARRAY** /*custom*/)
    {
       return S_OK;
    }
   STDMETHOD(OnBeginShutdown)(SAFEARRAY** /*custom*/)
  {
       return S_OK;
    }

In my case, no need to do anything in response to the extensibility callbacks apart from to say, yup, OK, continue on your way. IRibbonExtensibility has only one methid, GetCustomUI, which is pretty much the same as the managed one I've shown before - the details are slightly different, this being C++, so it's not as convenient as simply returning a string:

 STDMETHODIMP CNoReplyAllLite::GetCustomUI(BSTR ribbonID, BSTR* ribbonXml)
 {
 wchar_t* xml;
   if (     lstrcmpW(ribbonID, L"Microsoft.Outlook.Mail.Compose") == 0 ||
           lstrcmpW(ribbonID, L"Microsoft.Outlook.MeetingRequest.Send") == 0 ||
            lstrcmpW(ribbonID, L"Microsoft.Outlook.Post.Compose") == 0)
        xml = composeRobbomXml
L"<?xml version='1.0' encoding='UTF-8'?>"
L"<customUI xmlns='https://schemas.microsoft.com/office/2006/01/customui' loadImage='Ribbon_LoadImage'>"
L" <ribbon>"
L"  <tabs>"
L"   <tab idMso='TabNewMailMessage'>"
L"    <group id='grpCReply' label='Disable'>"
L"     <toggleButton id='btnCNoReplyAll' tag='2'"
L"                   size='large' image='2'"
L"                   getPressed='ItemButton_IsPressed' onAction='ItemButton_Click'"
L"                   screentip='No Reply All' supertip='Prevent recipients within the same organisation from replying to all'"
L"                   label='Reply All' />"
L"     <toggleButton id='btnCNoReply' tag='1'"
L"                   size='normal' image='1'"
L"                   getPressed='ItemButton_IsPressed' onAction='ItemButton_Click'"
L"                   screentip='No Reply' supertip='Prevent recipients within the same organisation from replying'"
L"                   label='Reply' />"
L"     <toggleButton id='btnCNoForward' tag='3'"
L"                   size='normal' image='3'"
L"                   getPressed='ItemButton_IsPressed' onAction='ItemButton_Click'"
L"                   screentip='No Forward' supertip='Prevent recipients within the same organisation from forwarding the message'"
L"                   label='Forward' />"
L"    </group>"
L"   </tab>"
L"  </tabs>"
L" </ribbon>"
L"</customUI>";
    else
        xml = nullptr;

  *ribbonXml = xml ? SysAllocString(xml) : nullptr;
   return S_OK;
}

My IDispatch implementation takes care of matching the callbacks in that, such as ItemButton_IsPressed, to a C++ method:

 STDMETHODIMP CNoReplyAllLite::GetIDsOfNames( REFIID, OLECHAR** names, UINT num, LCID lcid, DISPID* dispids )
 {
 HRESULT hr = S_OK;
  for( UINT i = 0; i < num; ++i )
      if( CompareStringW( lcid, 0, names[ i ], -1, L"ItemButton_Click", -1 ) == CSTR_EQUAL )
          dispids[i] = DISPID_ITEMBUTTON_ONACTION;
        else if( CompareStringW( lcid, 0, names[ i ], -1, L"ItemButton_IsPressed", -1 ) == CSTR_EQUAL )
         dispids[i] = DISPID_ITEMBUTTON_GETPRESSED;
      else if( CompareStringW( lcid, 0, names[ i ], -1, L"Ribbon_LoadImage", -1 ) == CSTR_EQUAL )
         dispids[i] = DISPID_RIBBON_LOADIMAGE;
       else
        {
           dispids[i] = DISPID_UNKNOWN;
            hr = DISP_E_UNKNOWNNAME;
        }
   return hr;
}

STDMETHODIMP CNoReplyAllLite::Invoke( DISPID dispid, REFIID, LCID, WORD /*flags*/, DISPPARAMS* params, VARIANT* result, EXCEPINFO*, UINT* )
{
 switch( dispid )
    {
       case DISPID_ITEMBUTTON_ONACTION:
            {
               // Remember: rgvarg array in *reverse* order
                Office::IRibbonControlPtr control = params->rgvarg[1].pdispVal;
              ItemButton_Click(control, params->rgvarg[0].boolVal);
            }
           break;

      case DISPID_ITEMBUTTON_GETPRESSED:
          {
               Office::IRibbonControlPtr control = params->rgvarg[0].pdispVal;
              variant_t b = ItemButton_IsPressed(control);
                *result = b.Detach();
           }
           break;

      case DISPID_RIBBON_LOADIMAGE:
           {
               IDispatchPtr pic = Ribbon_LoadImage(params->rgvarg[0].bstrVal);
              variant_t disp = pic.Detach();
              *result = disp.Detach();
            }
           break;
  }

   return S_OK;
}

Notice that rather than "raw" VARIANTs and COM pointers, I favour variant_t and the _com_ptr_t types: these wrap and hide the details of cleanup and, in the case of the latter, QueryInterface too. (Without these, I'd almost certainly make more use of ATL, because it provides similar wrappers.)

Taking a small step back: in order to be able to access the extensibility and Office interfaces, I need to import their type libraries, which requires the following -

 // IDTExtensibility2
#import "libid:AC0714F2-3D04-11D1-AE7D-00A0C90F26F4" auto_rename auto_search raw_interfaces_only rename_namespace("AddinDesign")
 // Office type library (i.e. mso.dll)
#import "libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52" auto_rename auto_search raw_interfaces_only rename_namespace("Office")

// Outlook type library (i.e. msoutl.olb)
#import "libid:00062FFF-0000-0000-C000-000000000046" auto_rename auto_search rename_namespace("Outlook") rename("GetOrganizer", "GetOrganizerAddressEntry")

There are two ways to #import a library - by filename or by registered library id. The latter is more useful in that it doesn't require a fixed absolute path (which will change for different versions of Office, or non default installations). Unfortunately, using the libid versions confuses IntelliSense, and the Visual Studio editor doesn't recognise any of the type names, but that is only a problem in that erroneous red squigglies appear. #import can also make use of the helper classes mentioned above, but that's only useful when calling into the library as opposed to implementing interfaces defined there - here, that means that raw makes sense for the Office and extensibility libraries. The "cooked" definitions are useful for the Outlook object model library, but notice the rename at the end - without that the imported file ends up with two instances of GetOrganizer which differ only in return type, a C++ error.

Here are the handlers for the click and is-pressed callbacks (error checking omitted for brevity):

 void CNoReplyAllLite::ItemButton_Click(Office::IRibbonControl* control, VARIANT_BOOL pressed)
{
    IDispatchPtr item = GetItemFromButton(control);
 Outlook::ActionPtr action = GetActionFromButton(control, item);
 VARIANT_BOOL enabled = pressed ? VARIANT_FALSE : VARIANT_TRUE;
  HRESULT hr = action->put_Enabled(enabled);
}

VARIANT_BOOL CNoReplyAllLite::ItemButton_IsPressed(Office::IRibbonControl* control)
{
  IDispatchPtr item = GetItemFromButton(control);
 Outlook::ActionPtr action = GetActionFromButton(control, item);
 VARIANT_BOOL enabled;
   HRESULT hr = action->get_Enabled(&enabled);
  return (SUCCEEDED(hr) && enabled) ? VARIANT_FALSE : VARIANT_TRUE;
}

There are definitely too many forms of boolean in Windows C++ - bool, BOOL and VARIANT_BOOL, all of which are different base types and have different values for true, hence the (perhaps overly pedantic) conversions between bool and VARIANT_BOOL above.

As with the VSTO implementation, I follow a short route from the ribbon control to get hold of the item it's associated with:

 static IDispatchPtr GetItemFromButton(Office::IRibbonControl* control)
{
   HRESULT hr;
 IDispatchPtr context;
   hr = control->get_Context(&context);
 Outlook::_InspectorPtr inspector = context;
 IDispatchPtr item;
  hr = inspector->get_CurrentItem(&item);
  return item;
}

This is where things get a bit tricky... As mentioned before, the various Outlook items which appear in inspector windows do not share a common base class. I don't want to try QueryInterface for all of them, and then use almost identical code (but for the type) to access the Action associated with each. In the managed case, I used reflection instead and, here, I do more or less the same, via the item's IDispatch interface:

 static Outlook::ActionPtr GetActionFromButton(Office::IRibbonControl* control, IDispatch* item)
{
  HRESULT hr;
 BSTR bstr = nullptr;
    hr = control->get_Tag(&bstr);
    _bstr_t tag(bstr, false);
   int index = bstr[0] - L'0';
 wchar_t* actionsName = L"Actions";
  DISPID id;
  hr = disp->GetIDsOfNames(IID_NULL, &actionsName, 1, LOCALE_SYSTEM_DEFAULT, &id);
 if (FAILED(hr))
     return nullptr;
 DISPPARAMS params = {0};
    variant_t result;
   hr = disp->Invoke(id, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &params, &result, nullptr, nullptr);
    Outlook::ActionsPtr actions = result;
   Outlook::ActionPtr action;
  variant_t actionIndex = index;
  hr = actions->Item(actionIndex, &action);
    return action;
}

Quite a lot uglier than the C# code I've shown before, but it's doing the same thing.

The final task for the code is button images. Again, the C++ world is similar to the C# one, just a few more hoops to jump through. I've embedded icons for the three buttons, with resource ids 101-103, and the ribbon's loadImage callback grabs the resource and converts to an IPictureDisp to be displayed - although OleCreatePictureIndirect does indeed create one of those, I only actually need the IDispatch pointer here.

 IDispatchPtr CNoReplyAllLite::Ribbon_LoadImage(BSTR imageName)
{
   int resourceId = 100 + imageName[0] - L'0';

 PICTDESC desc = {sizeof(PICTDESC)};
 desc.picType = PICTYPE_ICON;
    desc.icon.hicon = LoadIcon(g_hInst, MAKEINTRESOURCE(resourceId));
   assert(desc.icon.hicon);

    IDispatchPtr pic;
   HRESULT hr = OleCreatePictureIndirect(&desc, __uuidof(IDispatch), TRUE, (void**)&pic);

  DestroyIcon(desc.icon.hicon);

   return pic;
}

With all that in place, I have a native NoReplyAll implementation. It was more work than the VSTO one, up until the time I realised I had COM object locking problems in the managed version, which then became more complex and messier! Would I write any future add-ins in C++ or C#? I guess it depends: for something as teeny as this, especially if I wanted it to work across multiple Office applications, then native C++ could be a good idea. However, for anything of any complexity, the convenience of managed code really wins. One other benefit of the managed route is that the same assembly can be used in 32 and 64 bit Office: with native code, I do need to build separate DLLs for each.

And the final question is: is anyone sufficiently interested in this that I ought to make it a release alongside the managed download?