2011-05-27 240 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的直接窗口看到了什么?