5

I try to read the information of Microsoft Access (Office) CommandBars contained in an *.mdb. I could use Microsoft.Office.Interop.Access for this; however, these PIA assemblies are tied to specific Office versions. Therefore, to be version independent, I do it the late-bound way through C#'s dynamic type. I.e., I have no references to Microsoft Office specific assemblies. The price for this is that the access to the command bars is now weakly typed.

This approach works well, except when I try to access the custom button images. This is a condensed version of my real code to illustrate the problem:

dynamic access =  Activator.CreateInstance(Type.GetTypeFromProgID("Access.Application", true));
// Starts an Access XP (2002) process in my case, but could be any version.

foreach (dynamic commandBar in access.CommandBars) {
    if (!commandBar.BuiltIn) { // Only my menus and toolbars.
        foreach (dynamic control in commandBar.Controls) {
            if (control.Type == (int)MsoControlType.msoControlButton && !control.BuiltInFace) {
                string caption = control.Caption; // Works.
                stdole.IPictureDisp picture = control.Picture; // <==== Throws exception! ====
                // ...
            }
        }
    }
}

Calling the getter of the CommandBarButton.Picture property throws this exception:

Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED))

at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo) at System.Dynamic.ComRuntimeHelpers.CheckThrowException(Int32 hresult, ExcepInfo& excepInfo, UInt32 argErr, String message)
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
at MyApplication.MyMethod in MyApplication.cs:line 64

How can I get the picture avoiding this exception?

Note, this question is not about converting the IPictureDisp object to a System.Drawing.Image object. I already have a solution for this. Also, it makes no difference whether picture is typed as object, dynamic, or IPictureDisp. The property does exist, otherwise I would get the exception 'System.__ComObject' does not contain a definition for 'Picture'.

It is a .NET Framework 4.0 Windows Forms project compiled for x86 (32-bit).


Edit: Switching to Primary Interop Assemblies (PIA) allows strong typing, but does not solve the problem. The exception persists.

Meanwhile I have read more about PIAs. Since .NET Framework 4.0 you can tweak the properties of the assembly reference to make them version independent. Right click on References / Microsoft.Office.Interop.Access and

  • Set Embed Interop Types to True.
  • Set Specific Version to False.

Do this for References / office as well. Therefore I will be switching to PIAs instead of using dynamic.

8
  • "Dumb question": does the command bar control actually have a picture? Since the code is looping all the controls I just have to ask... And which version of Access is actually involved, here? A very quick look at the CommandBars.Control object (2010) doesn't reveal a Picture property, not even as a hidden member. A CommandBarButton, however, does have such a property. I think you need to determine the type and then cast to an object of the proper type. Commented Jan 28, 2020 at 17:31
  • The test control.Type == (int)MsoControlType.msoControlButton ensures that we have a CommandBarButton with such a property. I cannot test the type directly as it shows as System.__ComObject in the debugger. (And of course the real types are not available, since I have not referenced any Office libraries or PIA assemblies.) Commented Jan 28, 2020 at 17:47
  • If a property does not exist, the exception 'System.__ComObject' does not contain a definition for 'NonExistentProperty' is thrown. Commented Jan 28, 2020 at 17:53
  • I'm not arguing that it finds a control of that type. I'm saying you need to create a specific object of that type and assign the control to it with a cast so that you have an object that actually has a Picture property. VBA might make the conceptual leap (although I wouldn't count on it in this case - I seem to remember it didn't work, but it's been more than 10 years), C# in my experience isn't that flexible. Something along the lines of: Office.CommandBarButton cbb = (Office.CommandBarButton) control; Commented Jan 28, 2020 at 17:53
  • Note: VBA can do it, after a quick test. You might try PInvoke, instead, if you really want to return on the more generic Control. Commented Jan 28, 2020 at 17:59

2 Answers 2

0

I have no idea if this would help, but have you tried calling control.get_Picture()? I've had to explicitly use getter and setter methods instead of the properties for styles sometimes.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for the answer. Meanwhile I have gone another route. CommandBarButton buttons have a CopyFace() method copying the picture to the clipboard. I used it like this: Clipboard.Clear(); button.CopyFace(); if (Clipboard.ContainsImage()) { using Image image = Clipboard.GetImage(); .... I would prefer to get the image directly, but the clipboard solution also has the advantage that I don't have to convert a IPictureDisp object to a System.Drawing.Image.
0

It's been a few years since I published this question. But, since there is still interest in this question, I would like to present my solution here.

I have gone another route. CommandBarButton objects have a CopyFace() method copying the picture to the clipboard. I used it like this:

private Image Extract(CommandBarButton button, out string imageName)
{
    // The clipboard is not very reliable, so let's try several times.
    for (int tries = 0; tries < 10; tries++) {
        try {
            Clipboard.Clear();
            button.CopyFace();
            if (Clipboard.ContainsImage()) {
                using Image image = Clipboard.GetImage();
                if (image is not null) {
                    // Then I do some processing specific to my application
                    // using methods not shown here.
                    Image maskedImage = MakeTransparentContours(image);
                    if (!IsEmptyImage(maskedImage)) {
                        imageName = UniqueImageName(maskedImage);
                        return maskedImage;
                    }
                }
            } else {
                break;
            }
        } catch {
        }
        Thread.Sleep(100);  // Today I would probably do an async call.
    }
    imageName = null;
    return null;
}

When I see this old code, I see things I could have done differently, like using a TryExtract approach or returning the image and its name in a value tuple, use nullable reference types and what not...

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.