How to connect an open excel application in net.core 3.1?

Viewed 839

In Net.Framework I can use next function to operate with open instance of excel application:

using Excel = Microsoft.Office.Interop.Excel;
public static Excel.Workbook GetOpenedExcelWorkbook(string workBookName)
{
    Excel.Application xlsApp = null;
    try { xlsApp = (Excel.Application)Marshal.GetActiveObject("Excel.Application"); }
    catch { return null; }

    foreach (Excel.Workbook wb in xlsApp.Workbooks)
    {
        if (wb.Name == workBookName)
            return wb;
    }
    return null;
}

It is convenient, because an user can just click buttons in excel worksheet and call all necessary procedures. And I can write code for operate with excel in C#, which is more useful and powerful than VBA. Next code in VBA is used to call C# console application at click on the buttons in excel. It find apropriate exe-file near to active excel file and call it througth Shell command in VBA with necessary arguments.

Sub Button1_Click()
    Call ExeCaller("ConsoleApp.exe", "Button1_action_arg")
End Sub


Sub ExeCaller(ExeName As String, Optional cmd As String = "")
    Dim arg As String
    If Not Dir(ExeName) = "" Then
        arg = ExeName & " " & ActiveWorkbook.Name & IIf(cmd = "", "", " " & cmd)
    Else
        Dim pathToWB As String: pathToWB = ThisWorkbook.Path
        Dim pathToExe_Release As String: pathToExe_Release = pathToWB + "\bin\Release\" + ExeName
        Dim pathToExe_Debug As String:   pathToExe_Debug = pathToWB + "\bin\Debug\" + ExeName

        Dim dr As Double, dd As Double
        If Not Dir(pathToExe_Release) = "" Then dr = FileDateTime(pathToExe_Release)
        If Not Dir(pathToExe_Debug) = "" Then dd = FileDateTime(pathToExe_Debug)
        Dim dmax As Double: dmax = IIf(dr > dd, dr, dd)

        If (dmax = 0) Then
            If Not Dir(ExeName) = "" Then
                pathToExe = ExeName
            Else
                MsgBox "File " + ExeName + "not found!", vbCritical, Error
                Exit Sub
            End If
        ElseIf dmax = dr Then
            pathToExe = pathToExe_Release
        ElseIf dmax = dd Then
            pathToExe = pathToExe_Debug
        Else

            MsgBox "File " + ExeName + "not found!", vbCritical, Error
            Exit Sub
        End If

        arg = pathToExe & " " & ActiveWorkbook.Name & IIf(cmd = "", "", " " & cmd)
    End If

    Shell arg, vbNormalFocus
End Sub

The key functionality than make it possible is Marshal.GetActiveObject(string ProgID) in C#, that is available in Net.Framework.

Now I try to migrate to Net.Core 3.1. It is possible to use Excel object model in Net.Core by this way: https://github.com/dotnet/samples/tree/master/core/extensions/ExcelDemo So that I can create a new instance of Excel.Application and through it create new or open exist excel workbook and operate with it with all functionality. BUT I can't connect to open excel workbook because there is no Marshal.GetActiveObject(string ProgID) function in Net.Core. Preview Net.Core 5 doesn't have it either. And there is no similar function.

I tried to do it with Marshal.GetObjectForIUnknown(IntPtr pUnk) whith is available in Net.Core. As I know Excel.Application is a COM-object and therefore it have IUnknown interface. I tried to get this pointer throught a process of open excel application that can be found by name. I used this naive code:

public static Excel.Application GetOpenedExcelApplication()
{
    Process[] aP = Process.GetProcesses();
    foreach (Process p in aP)
    {
        string pn = p.ProcessName.ToLower();

        if (Regex.IsMatch(pn, "excel*"))
        {
            object o = Marshal.GetObjectForIUnknown(p.Handle);
            Excel.Application app = (Excel.Application)o;
            return app;
        }
    }

    return null;
}

A System.ExecutionEngineException occurred on the Marshall.GetObjectForIUnknown() call line.

Is there a way in Net.Core to get access the open Excel workbook?

1 Answers

We can have a look to source code of Marshal class in .NET Framework 4.X and copy GetActiveObject method implementation to projects of .NET Core and .NET 5 and .NET 6. It works.

public static class ExMarshal
{
    internal const String OLEAUT32 = "oleaut32.dll";
    internal const String OLE32 = "ole32.dll";

    [System.Security.SecurityCritical]  // auto-generated_required
    public static Object GetActiveObject(String progID)
    {
        Object obj = null;
        Guid clsid;

        // Call CLSIDFromProgIDEx first then fall back on CLSIDFromProgID if
        // CLSIDFromProgIDEx doesn't exist.
        try
        {
            CLSIDFromProgIDEx(progID, out clsid);
        }
        //            catch
        catch (Exception)
        {
            CLSIDFromProgID(progID, out clsid);
        }

        GetActiveObject(ref clsid, IntPtr.Zero, out obj);
        return obj;
    }

    //[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)]
    [DllImport(OLE32, PreserveSig = false)]
    [ResourceExposure(ResourceScope.None)]
    [SuppressUnmanagedCodeSecurity]
    [System.Security.SecurityCritical]  // auto-generated
    private static extern void CLSIDFromProgIDEx([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid);

    //[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)]
    [DllImport(OLE32, PreserveSig = false)]
    [ResourceExposure(ResourceScope.None)]
    [SuppressUnmanagedCodeSecurity]
    [System.Security.SecurityCritical]  // auto-generated
    private static extern void CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid);

    //[DllImport(Microsoft.Win32.Win32Native.OLEAUT32, PreserveSig = false)]
    [DllImport(OLEAUT32, PreserveSig = false)]
    [ResourceExposure(ResourceScope.None)]
    [SuppressUnmanagedCodeSecurity]
    [System.Security.SecurityCritical]  // auto-generated
    private static extern void GetActiveObject(ref Guid rclsid, IntPtr reserved, [MarshalAs(UnmanagedType.Interface)] out Object ppunk);
}
Related