1

I want to rebuild a OpenWith context menu.

So far i use the SHAssocEnumHandler to retrieve all possible applications for a given file extension.

Now i want to call the Invoke method of an chosen AssocHandler. This method needs a pointer to a IDataObject to call the application with the given Object.

In my case i have the path to a file i want to open:

c:\devtest\htmlTest.html

My Problem is how do i get from this Path to an IDataObject. And then a pointer to this Object.

My Code so far:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace AssocHandlerWithPointer
{
  [Flags]
  public enum ASSOC_FILTER
  {
    ASSOC_FILTER_NONE = 0x00000000,
    ASSOC_FILTER_RECOMMENDED = 0x00000001
  }

  public class Test
  {
    [DllImport("Shell32", EntryPoint = "SHAssocEnumHandlers", PreserveSig = false)]
    public extern static void SHAssocEnumHandlers([MarshalAs(UnmanagedType.LPWStr)] string pszExtra, ASSOC_FILTER afFilter, [Out] out IntPtr ppEnumHandler);

    // IEnumAssocHandlers
    [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Unicode)]
    private delegate int FuncNext(IntPtr refer, int celt, [Out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.Interface, SizeParamIndex = 1)] IntPtr[] rgelt, [Out] out int pceltFetched);

    // IAssocHandler
    [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Unicode)]
    private delegate int FuncGetUiName(IntPtr refer, out IntPtr ppsz);

    [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Unicode)]
    private delegate int FuncInvoke(IntPtr refer, IntPtr pdo);

    static void Main(string[] args)
    {
      const string extension = ".html";

      IntPtr pEnumAssocHandlers;
      SHAssocEnumHandlers(extension, ASSOC_FILTER.ASSOC_FILTER_RECOMMENDED, out pEnumAssocHandlers);

      IntPtr pFuncNext = Marshal.ReadIntPtr(Marshal.ReadIntPtr(pEnumAssocHandlers) + 3 * sizeof(int));
      FuncNext next = (FuncNext)Marshal.GetDelegateForFunctionPointer(pFuncNext, typeof(FuncNext));

      IntPtr[] pArrayAssocHandlers  = new IntPtr[255];
      int num;

      int resNext = next(pEnumAssocHandlers, 255, pArrayAssocHandlers, out num);
      if (resNext == 0)
      {
        for (int i = 0; i < num; i++)
        {
          IntPtr pAssocHandler = pArrayAssocHandlers[i];

          IntPtr pFuncGetUiName = Marshal.ReadIntPtr(Marshal.ReadIntPtr(pAssocHandler) + 4 * sizeof(int));
          FuncGetUiName getUiName = (FuncGetUiName)Marshal.GetDelegateForFunctionPointer(pFuncGetUiName, typeof(FuncGetUiName));
          IntPtr pUiName;
          int resGetUiName = getUiName(pAssocHandler, out pUiName);
          Console.WriteLine("UI: " + Marshal.PtrToStringUni(pUiName));

          const string filePath = "c:\\devtest\\htmlTest.html";
          DataObject dataObject = new DataObject();
          dataObject.SetData(filePath);

          IntPtr pDataObject = Marshal.GetIUnknownForObject(dataObject);
          IntPtr pFuncInvoke = Marshal.ReadIntPtr(Marshal.ReadIntPtr(pAssocHandler) + 5 * sizeof(int));
          FuncInvoke invoke = (FuncInvoke) Marshal.GetDelegateForFunctionPointer(pFuncInvoke, typeof (FuncInvoke));
          int resInvoke = invoke(pAssocHandler, Marshal.ReadIntPtr(pDataObject));
          Console.WriteLine(resInvoke);
          Console.WriteLine("-----");

          Marshal.Release(pArrayAssocHandlers[i]);
        }
      }
      Marshal.Release(pEnumAssocHandlers);
      Console.ReadLine();
    }
  }
}

I use the DataObject from System.Windows.Forms. What should be ok because DataObject inherits from IDataObject.

I know calling the Invoke method inside the loop makes no sens for the use case. And also there is no user interaction. But to get it to work it should be ok.

Note:
The solution has to work an Windows 7 and Windows 10.

Parsing the Registry manually and retrieve the shell/open/command is no possible solution.

fdafadf
  • 809
  • 5
  • 13
Patrick
  • 341
  • 4
  • 15

1 Answers1

0

The fundamental object to know when you want to work with the shell is IShellItem. From here you can get other interfaces using the BindToHandler method.

To create the initial IShellItem from a path, you can use SHCreateItemFromParsingName.

You should use normal COM interfaces definitions instead of trying to build v-tables by hand. Here is a sample code that seems to work:

class Program
{
    [STAThread] // don't forget STAThread, most shell methods require that
    static void Main(string[] args)
    {
        string path = @"d:\kilroy_was_here\test.htm";

        int hr = SHCreateItemFromParsingName(path, null, typeof(IShellItem).GUID, out IShellItem item);
        if (hr != 0)
            throw new Win32Exception(hr);

        hr = SHAssocEnumHandlers(Path.GetExtension(path), ASSOC_FILTER.ASSOC_FILTER_RECOMMENDED, out IEnumAssocHandlers eah);
        if (hr != 0)
            throw new Win32Exception(hr);

        do
        {
            eah.Next(1, out IAssocHandler handler, out int fetched);
            if (fetched != 1)
                break;

            handler.GetName(out string name);
            Console.WriteLine(name);
            if (name.Contains("firefox")) // open with firefox, for example
            {
                var BHID_DataObject = new Guid("b8c0bd9f-ed24-455c-83e6-d5390c4fe8c4");
                var dao = item.BindToHandler(null, BHID_DataObject, typeof(IDataObject).GUID);
                handler.Invoke(dao);
            }
        }
        while (true);
    }

    [Flags]
    public enum ASSOC_FILTER
    {
        ASSOC_FILTER_NONE = 0x00000000,
        ASSOC_FILTER_RECOMMENDED = 0x00000001
    }

    [Guid("973810ae-9599-4b88-9e4d-6ee98c9552da"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IEnumAssocHandlers
    {
        [PreserveSig]
        int Next(int celt, out IAssocHandler rgelt, out int pceltFetched);
    }

    [Guid("f04061ac-1659-4a3f-a954-775aa57fc083"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IAssocHandler
    {
        void GetName([MarshalAs(UnmanagedType.LPWStr)] out string ppsz);
        void GetUIName([MarshalAs(UnmanagedType.LPWStr)] out string ppsz);
        void GetIconLocation([MarshalAs(UnmanagedType.LPWStr)] out string ppszPath, out int pIndex);
        [PreserveSig]
        int IsRecommended();
        void MakeDefault([MarshalAs(UnmanagedType.LPWStr)] string pszDescription);
        void Invoke(IDataObject pdo);
        void CreateInvoker(IDataObject pdo, out /*IAssocHandlerInvoker*/ object invoker);
    }

    [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IShellItem
    {
        // here we only need this member
        IDataObject BindToHandler(IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, [MarshalAs(UnmanagedType.LPStruct)] Guid riid);
    }

    [DllImport("shell32", CharSet = CharSet.Unicode)]
    private extern static int SHCreateItemFromParsingName(string pszPath, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem ppv);

    [DllImport("shell32", CharSet = CharSet.Unicode)]
    private extern static int SHAssocEnumHandlers(string pszExtra, ASSOC_FILTER afFilter, out IEnumAssocHandlers ppEnumHandler);
}

PS: I used IDataObject from System.Runtime.InteropServices.ComTypes

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • Hey Simon, your code works fine on an Windows 10 machine. On an Windows 7 machine i get a COMException with a unknown error when calling the SHAssocEnumHandlers. See also this question: [link](https://stackoverflow.com/questions/48400038/use-shassocenumhandlers-in-c-sharp/48449462#48449462) – Patrick Jan 26 '18 at 13:43
  • what's the error number? have you put the STAThread attribute? – Simon Mourier Jan 26 '18 at 14:44
  • The error number is: `HRESULT: 0x80004005 (E_FAIL)`. I put the [STAThread] attribute, i also experimented with the ´[PreserveSig]` nothing change. – Patrick Jan 26 '18 at 14:48
  • ok, I can reproduce on Win7. Wow, that's weird, I understand why you have coded this call the way you did. I tested C++, it works fine, as expected. In fact it crashes in the CLR marshaling layer as soon as we want to get back this particular COM object (it works with object and then fails if we want to cast). Definitely looks like a bug. Maybe the Win7 registry misses some crucial information about that interface ... Anyway, the rest of my answer should be correct about how to build an IDataObject – Simon Mourier Jan 26 '18 at 18:07
  • In the `IShellItem::BindToHandler` method is a `BHID_EnumAssocHandlers` available maybe we need to use this on the SHAssocEnumHandlers output. But i don´t have an idea how. – Patrick Jan 29 '18 at 15:59
  • Well spotted. Well, it's exactly like the one with BHID_DataObject+IDataObject, but with BHID_EnumAssocHandlers+IEnumAssocHandlers instead – Simon Mourier Jan 29 '18 at 17:08
  • What is the significance of this GUID? var BHID_DataObject = new Guid("b8c0bd9f-ed24-455c-83e6-d5390c4fe8c4"); Microsoft's documentation also appears to be missing for that parameter: https://learn.microsoft.com/en-us/windows/desktop/api/shobjidl_core/nf-shobjidl_core-ishellitem-bindtohandler – Alec Jul 02 '18 at 03:01