2011-05-27 52 views
0

我使用Robert Giesecke http://sites.google.com/site/robertgiesecke/Home/uploads/unmanagedexports的解決方案將函數從託管代碼導出到非託管代碼。該解決方案工作得很好,但在辦公室使用該解決方案時出現問題(excel)。從C#導出C函數並在VBA中使用它

我試圖開發一種

  • 使用SQLAuthentication
  • 通過數據庫
  • 的名字傳遞的SQL語句
  • 並返回結果連接到SQLServer的
  • 一個DLL

因此,第e DLL無法看到密碼,我知道它可以通過使用特殊工具來完成。這樣做對我們的要求就足夠了。

在C#代碼:

using System; 
using System.Collections.Generic; 
using System.Text; 
using RGiesecke.DllExport; 
using ADODB; 
using System.Xml; 
using System.IO; 
using System.Security.Cryptography; 
using System.Runtime.InteropServices; 
using System.Windows.Forms; 

namespace SqlConRVT 
{ 
public static class SqlConRVT 
{ 
    [DllExport("SqlConRVT", CallingConvention = CallingConvention.StdCall)] 
    [return: MarshalAs(UnmanagedType.IDispatch)] 
    public static Object OpenRecordset ([MarshalAs(UnmanagedType.AnsiBStr)] string databaseName, 
    [MarshalAs(UnmanagedType.AnsiBStr)] string commandText) 
    { 
    if (String.IsNullOrEmpty(databaseName)) throw new ArgumentNullException("databaseName"); 
    if (String.IsNullOrEmpty(commandText)) throw new ArgumentNullException("commandText"); 
    try 
    { 
     var connection = new ADODB.Connection(); 
     var intConnectionMode = (int) ConnectModeEnum.adModeUnknown; 
     var username = Crypto.DecryptMessage("XEj0PC2lMIs=", "FinON"); 
     var password = Crypto.DecryptMessage("7YIDPO7eBoFAhskAX6JGAg==", "FinON"); 
     connection.Open("Provider='SQLOLEDB';Data Source='PETER-PC\\SQLEXPRESS'; Initial Catalog='" + databaseName + "';", username, password, intConnectionMode); 
     var rs = new Recordset(); 
     rs.Open(commandText, connection, CursorTypeEnum.adOpenForwardOnly, LockTypeEnum.adLockOptimistic, -1); 
     return rs; 
    } 
    catch (Exception ex) 
    { 
     // an exception in a DLL will most likely kill the excel process 
     // we really dont want that to happen 
     MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error); 
     return null; 
    } 
} 
} 

public partial class Crypto 
{ 
    public static string DecryptMessage(string encryptedBase64, string password) 
    { 
     TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider(); 
     des.IV = new byte[8]; 
     PasswordDeriveBytes pdb = new PasswordDeriveBytes(password, new byte[0]); 
     des.Key = pdb.CryptDeriveKey("RC2", "MD5", 128, new byte[8]); 
     byte[] encryptedBytes = Convert.FromBase64String(encryptedBase64); 
     MemoryStream ms = new MemoryStream(encryptedBase64.Length); 
     CryptoStream decStream = new CryptoStream(ms, des.CreateDecryptor(), CryptoStreamMode.Write); 
     decStream.Write(encryptedBytes, 0, encryptedBytes.Length); 
     decStream.FlushFinalBlock(); 
     byte[] plainBytes = new byte[ms.Length]; 
     ms.Position = 0; 
     ms.Read(plainBytes, 0, (int)ms.Length); 
     decStream.Close(); 
     return Encoding.UTF8.GetString(plainBytes); 
    } 
} 
} 

我在VBA代碼:

Declare Function SqlConRVT Lib _ 
"C:\Users\Administrator\Documents\Visual Studio 2008\Projects\SqlConRVT\SqlConRVT\bin\Debug\x86 \SqlConRVT.dll" (ByVal databaseName As String, ByVal commandText As String) As Object 

Sub SQLCon() 
Dim x As Object 
x = SqlConRVT("Adressen", "Select * from tblAdressen") 
End Sub 

在C#DLL和我引用的所有客戶端應用程序 「Microsoft ActiveX數據對象2.8庫」。

我試圖用導出的64位DLL與C#,工作正常。 我試圖使用導出的64位DLL作爲靜態類與C#,工作正常。 我試圖用導出的32位DLL與VB6,應用程序崩潰。 我試圖用導出的32位DLL與VBA(Excel),應用程序崩潰。

我用依賴關係walker檢查了32位DLL中導出函數的存在。

爲什麼我不能在office(Excel)中使用32位DLL?


當然,我有32位的辦公室!

你的「簡化例子」工作正常,班級正確回饋!

我減少我的例子:

using System; 
using System.Collections.Generic; 
using System.Text; 
using RGiesecke.DllExport; 
using ADODB; 
using System.Xml; 
using System.IO; 
using System.Security.Cryptography; 
using System.Runtime.InteropServices; 
using System.Windows.Forms; 

[ComVisible(true), ClassInterface(ClassInterfaceType.AutoDual)] 

static class SqlConRVT 
{ 
    [DllExport(CallingConvention = CallingConvention.StdCall)] 
    [return: MarshalAs(UnmanagedType.IDispatch)] 
    //[return: MarshalAs(UnmanagedType.I4)] 
    //[return: MarshalAs(UnmanagedType.AnsiBStr)] 

    static Object GetNewObject([MarshalAs(UnmanagedType.AnsiBStr)] String databaseName, 
    [MarshalAs(UnmanagedType.AnsiBStr)] String commandText) 
    { 
     var test = new StreamReader("C:\\lxbu.log"); 
     return test; 
     //var rs = new Recordset(); 
     //return rs; 
     //int A = 1; 
     //return A; 
     //String A = commandText; 
     //return A; 
    } 
} 

我在VBA代碼:

Declare Function GetNewObject Lib "C:\Users\Administrator\Documents\Visual Studio 2008\Projects\An\An\bin\Debug\x86\An.dll" (ByVal databaseName As String, ByVal commandText As String) As Object 

Sub An1() 
Dim x As Object 
Set x = GetNewObject("Adressen", "Select * from tblAdressen") 
End Sub 

如果我試圖返回一個int值 - >工作兩不誤! 如果我嘗試返回一個字符串值 - >工作正確! 如果我嘗試返回一個對象(例如記錄集對象或流讀取器對象),Excel崩潰?必須有一個愚蠢的小錯誤!


謝謝羅伯特 - 因爲每次你的代碼是完美的!我可以看到StreamReader對象的內容,如果我在VBA

MsgBox instance.ReadtoEnd() 

使用下面的代碼,結果是:

「ABC AO〜EEE @dkfjf - >添加來回VBA」

問題是最初的ADODB.connection !!!!!

[DllExport(CallingConvention = CallingConvention.StdCall)] 
[return: MarshalAs(UnmanagedType.IDispatch)] 
static Object GetNewObject([MarshalAs(UnmanagedType.LPStr)] String databaseName, [MarshalAs(UnmanagedType.LPStr)] String commandText) 
{ 
    //if (String.IsNullOrEmpty(databaseName)) throw new ArgumentNullException("databaseName"); 
    //if (String.IsNullOrEmpty(commandText)) throw new ArgumentNullException("commandText"); 
    { 
     var connection = new ADODB.Connection(); 
     //var rs = new Recordset(); 
     StreamReader sr = new StreamReader("C:\\lxbu.log"); 
     //var intConnectionMode = (int)ConnectModeEnum.adModeUnknown; 
     //var username = "..."; 
     //var password = "........."; 
     //connection.Open("Provider='SQLOLEDB';Data Source='PETER-PC\\SQLEXPRESS'; Initial Catalog='" + databaseName + "';", username, password, intConnectionMode); 
     //rs.Open(commandText, connection, CursorTypeEnum.adOpenForwardOnly, LockTypeEnum.adLockOptimistic, -1); 
     return sr; 
    } 
} 

如果我使用「var connection = new ADODB.Connection();」 Excel崩潰。問題是在32位的DLL使用ADODB(C#,並使用64位-DLL沒有問題)。你的解決方案沒有問題(!!!)!

+2

爲什麼不使用VSTO或COM。考慮到未經管理的土地使事情變得比你需要的更復雜。 – 2011-05-27 19:35:39

+0

@David:COM是非託管的,但是對於從VBA訪問C#庫而言,我肯定會建議COM路由。 – 2011-05-27 21:18:07

+0

COM需要註冊,需要管理權限。有時候這是不可能的...... – aurel 2014-03-04 14:14:22

回答

1

我不能回答你的問題,但也許你正在使用第三方代碼有無證限制或假設在Office中運行時,不工作。

但也有其他方式可以將託管API導出到Excel VBA。我使用的解決方案如下:

  • 在IDL中爲要從.NET公開的API定義一組雙重COM接口。應該有一個主要的Factory接口可以用作VBA的主入口點。工廠必須能夠直接或間接實例化任何想要公開的對象(下面描述的技術不允許VBA直接創建對象,因此您需要使用Factory類來實現)。

  • 生成從IDL類型庫使用MIDL.EXE,並使用TLBIMP,以將其暴露在.NET

  • 創建.NET類庫項目引用由TLBIMP生成COM互操作程序集,寫類實現API。

  • 創建VSTO項目。在ThisWorkbook.Workbook_Open事件處理程序,實例化的主要工廠對象並將其作爲參數傳遞給VBA宏:

    IMyMainFactory factory = // ... create factory 
    ThisApplication.Run("RegisterFactory", factory, Type.Missing, ...); 
    
  • 在VSTO工作簿,創建宏RegisterFactory並保存工廠類的實例在一個全局變量:

    Option Explicit 
    Private objFactory As Object 
    
    Public Sub RegisterFactory(Factory As Object) 
        Set objFactory = Factory 
    End Sub 
    
  • 構建VSTO應用程序,以及VSTO工作簿轉換爲XLA加載項。爲此,您可以使用VB,VBA或VBScript代碼是這樣的:

    Set w = Application.Workbooks.Open("MyVstoWorkbook.xls", ...) 
    w.IsAddin = True 
    w.SaveAs "MyVstoAddIn.xla", 18, ... 
    

以上的結果是,每當你加載外接MyVstoAddIn.xla,它將實例化你的工廠,並將其存儲在一個VBA模塊中的全局變量。您可以從VBA代碼訪問的(這也將有一個參照上面生成的類型庫),你是啓動和運行。

與標準COM Interop相比,它有許多優點 - 其中最重要的是您的VSTO加載項具有自己的AppDomain和應用程序配置文件,因此您不會與其他託管代碼發生衝突。

+0

我是彼得提到的解決方案的作者,我可以向你保證,我經歷了相當長的一段時間纔不依賴任何假設。但是,您的解決方案看起來很酷:-) – 2011-05-30 08:12:56

1

當我在郵件對話中問你:你真的使用64位的辦公室嗎?
這是不太可能,這就是爲什麼我要檢查的前期。

即使您使用64位Office,也應該可以工作。 有一點你必須記住:當你從VBA調用DLL函數時,傳遞的字符串類型將是一個LPStr(指向Ansi字符的指針)。 AnsiBStr也應該這樣做。 但您定義的類將使用BStr,這是COM的標準。

下面是一個簡化示例,它既不需要MSSQL客戶端庫也不需要ADODB。 (所以故障少點) 免責聲明:雖然我有一個64位的Windows,我只安裝了一個86辦公室(2007):

[ComVisible(true), ClassInterface(ClassInterfaceType.AutoDual)] 
public class Sample 
{ 
    public string Text 
    { 
     [return: MarshalAs(UnmanagedType.BStr)] 
     get; 
     [param: MarshalAs(UnmanagedType.BStr)] 
     set; 
    } 

    [return: MarshalAs(UnmanagedType.BStr)] 
    public string TestMethod() 
    { 
     return Text + "..."; 
    } 
} 

static class UnmanagedExports 
{ 
    [DllExport(CallingConvention = CallingConvention.StdCall)] 
    [return: MarshalAs(UnmanagedType.IDispatch)] 
    static Object CreateDotNetObject([MarshalAs(UnmanagedType.LPStr)] String text) 
    { 
     try 
     { 
     return new Sample { Text = text }; 
     } 
     catch (Exception ex) 
     { 
     MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error); 
     return null; 
     } 
    } 
} 

這是如何從任何VBA上下的使用(例如, Excel或Access)

Declare Function CreateDotNetObject Lib "The full path to your assembly or just the assembly if it is accessible from Excel" (ByVal text As String) As Object 
Sub test() 

    Dim instance As Object 

    Set instance = CreateDotNetObject("Test 1") 
    Debug.Print instance.Text 

    Debug.Print instance.TestMethod 

    instance.text = "abc 123" ' case insensitivity in VBA works as expected 

    Debug.Print instance.Text 
End Sub 

如果這是爲你工作,我們可以從那裏無論你想去的地方。但是知道你有什麼樣的辦公室版本(CPU平臺),以及這個簡單的示例是否可以工作第一個是很重要的。

+0

我編輯了我的問題! – 2011-05-30 20:19:34

+0

彼得,我沒有檢查文檔,但我有點懷疑StreamReader是ComVisible(true)。 – 2011-05-30 22:22:09

+0

我討厭微軟!使用「Microsoft ActiveX數據對象2.5庫」而不是「Microsoft ActiveX Data Object 2.8 Libraray」,並且在WinXP和Windows 7(64位)上一切正常。非常感謝您Robert,您的解決方案非常棒! – 2011-06-02 16:25:50

1

我會問你一些點再次,因爲你還挺迴避一些問題(在這裏,並通過電子郵件之前),我真的不知道你真正的嘗試:

  • 您沒有設置的CPU平臺你的項目到x86?
  • 如果沒有,請在x86子文件夾中選擇程序集?
  • 你試過使用UnmanagedType.LPStr作爲你的字符串參數嗎?
    雖然我有過託管/非託管Interop的公平,但我從來沒有必須處理過VBA,所以我不確定有關AnsiBStr。

甚至不要裝什麼比x86的DLL一樣,不能工作。
自己動手,將項目的CPU平臺更改爲x86並刪除當前輸出文件夾。恐怕所有這些不同的版本都有所不同。 重建後,你應該只有x86一個,這應該工作得很好。

而只是爲了確保這個工作的精細部分:嘗試這一個,這是你說它沒有工作的一個變種。並且,儘量不要做其他事情。最好的測試方法是從我的模板中創建一個新項目,並將下面的代碼粘貼到示例導出類中。

C#

[DllExport] 
[return: MarshalAs(UnmanagedType.IDispatch)] 
static Object CreateDotNetObject([MarshalAs(UnmanagedType.LPStr)] String text) 
{ 
    try 
    { 
     var testFileName = Path.Combine(Path.GetTempPath(), "VbaTestFile.txt"); 
     if (!File.Exists(testFileName)) 
     File.WriteAllText(testFileName, "abc Äö ~éêè @dkfjf", Encoding.UTF8); 

     using (var writer = File.AppendText(testFileName)) 
     writer.WriteLine(text); 

     return new StreamReader(testFileName); 
    } 
    catch (Exception ex) 
    { 
     MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error); 
     return null; 
    } 
} 

VBA

Declare Function CreateDotNetObject Lib "Full path to your assembly" (ByVal text As String) As Object 

Sub Test() 

    Dim instance As Object 

    Set instance = CreateDotNetObject("-> Added fro VBA") 
    Debug.Print instance.ReadToEnd() 
    instance.Close 
End Sub 

那你在VBA的直接窗口看到了什麼?