实时(未保存)Excel数据和C#对象之间最快的接口方式

时间:2010-10-01 14:47:39

标签: c# excel automation vsto com-interop

我想知道从打开的Excel工作簿到c#对象读取和写入数据的最快方法是什么。背景是我想开发一个从Excel中使用的c#应用程序并使用excel中保存的数据。

业务逻辑将驻留在c#应用程序中,但数据将驻留在Excel工作簿中。用户将使用Excel并在Excel工作簿上单击按钮(或执行类似操作)以启动c#应用程序。然后,c#应用程序将从excel工作簿中读取数据,处理数据,然后将数据写回excel工作簿。
可能需要读取大量数据块并将其写回excel工作簿,但它们通常具有相对较小的大小,例如10行和20列。有时可能需要处理大量数据,大约50,000行和40列。

我知道使用VSTO这样说比较容易,但我想知道最快(但仍然健壮和优雅)的解决方案是什么,并了解速度。我不介意解决方案是否建议使用第三方产品或使用C ++。

显而易见的解决方案是使用VSTO或互操作,但我不知道性能与我目前用于读取数据的VBA相比,或者是否还有其他解决方案。

这是在专家交流中发布的,他说VSTO比VBA慢得多,但那是几年前我不知道性能是否有所改善。

http://www.experts-exchange.com/Microsoft/Development/VSTO/Q_23635459.html

感谢。

6 个答案:

答案 0 :(得分:40)

我认为这是一个挑战,并且打赌在Excel和C#之间移植数据的最快方法是使用Excel-Dna - http://exceldna.codeplex.com。 (免责声明:我开发Excel-Dna。但它仍然是真的......)

因为它使用本机.xll接口,所以它会跳过您使用VSTO或其他基于COM的加载项方法所带来的所有COM集成开销。使用Excel-Dna,您可以创建一个连接到菜单或功能区按钮的宏,该按钮读取范围,处理它,并将其写回Excel中的范围。全部使用C#中的本机Excel界面 - 而不是COM对象。

我做了一个小测试函数,它将当前选择放入一个数组中,对数组中的每个数字进行平方,然后将结果从单元格A1开始写入Sheet 2。您只需添加(免费)Excel-Dna运行时,您可以从http://exceldna.codeplex.com下载。

我读了C#,处理并在一秒钟内写回一百万个单元格的Excel。这对你来说够快吗?

我的功能如下:

using ExcelDna.Integration;
public static class RangeTools {

[ExcelCommand(MenuName="Range Tools", MenuText="Square Selection")]
public static void SquareRange()
{
    object[,] result;

    // Get a reference to the current selection
    ExcelReference selection = (ExcelReference)XlCall.Excel(XlCall.xlfSelection);
    // Get the value of the selection
    object selectionContent = selection.GetValue();
    if (selectionContent is object[,])
    {
        object[,] values = (object[,])selectionContent;
        int rows = values.GetLength(0);
        int cols = values.GetLength(1);
        result = new object[rows,cols];

        // Process the values
        for (int i = 0; i < rows; i++)
        {
            for (int j = 0; j < cols; j++)
            {
                if (values[i,j] is double)
                {
                    double val = (double)values[i,j];
                    result[i,j] = val * val;
                }
                else
                {
                    result[i,j] = values[i,j];
                }
            }
        }
    }
    else if (selectionContent is double)
    {
        double value = (double)selectionContent;
        result = new object[,] {{value * value}}; 
    }
    else
    {
        result = new object[,] {{"Selection was not a range or a number, but " + selectionContent.ToString()}};
    }

    // Now create the target reference that will refer to Sheet 2, getting a reference that contains the SheetId first
    ExcelReference sheet2 = (ExcelReference)XlCall.Excel(XlCall.xlSheetId, "Sheet2"); // Throws exception if no Sheet2 exists
    // ... then creating the reference with the right size as new ExcelReference(RowFirst, RowLast, ColFirst, ColLast, SheetId)
    int resultRows = result.GetLength(0);
    int resultCols = result.GetLength(1);
    ExcelReference target = new ExcelReference(0, resultRows-1, 0, resultCols-1, sheet2.SheetId);
    // Finally setting the result into the target range.
    target.SetValue(result);
}
}

答案 1 :(得分:38)

答案 2 :(得分:4)

除了Mike Rosenblum关于阵列使用的评论之外,我想补充说我一直在使用非常方法(VSTO +数组),当我测量它时,实际读取速度本身就在几毫秒之内。只需记住在读/写之前禁用事件处理和屏幕更新,并记住在操作完成后重新启用。

使用C#,您可以创建与Excel VBA本身完全相同的基于1的阵列。这非常有用,特别是因为即使在VSTO中,当您从Excel.Range对象中提取数组时,该数组也是基于1的,因此将基于Excel的数组保持为1可以帮助您避免始终检查是否存在数组是从一开始的或从零开始的。   (如果数组中的列位置对您很重要,那么必须处理基于0和基于数组的数组可能会非常痛苦。)

通常将Excel.Range读入数组看起来像这样:

var myArray = (object[,])range.Value2;


我对Mike Rosenblum的数组写入的变体使用了这样的基于1的数组:

int[] lowerBounds = new int[]{ 1, 1 };
int[] lengths = new int[] { rowCount, columnCount };  
var myArray = 
    (object[,])Array.CreateInstance(typeof(object), lengths, lowerBounds);

var dataRange = GetRangeFromMySources();

// this example is a bit too atomic; you probably want to disable 
// screen updates and events a bit higher up in the call stack...
dataRange.Application.ScreenUpdating = false;
dataRange.Application.EnableEvents = false;

dataRange = dataRange.get_Resize(rowCount, columnCount);
dataRange.set_Value(Excel.XlRangeValueDataType.xlRangeValueDefault, myArray);

dataRange.Application.ScreenUpdating = true;
dataRange.Application.EnableEvents = true;

答案 3 :(得分:3)

Excel数据的最快接口是C API。有许多产品使用此接口将.NET链接到Excel。

我喜欢的2个产品是Excel DNA(免费和开源)和Addin Express(这是一个商业产品,同时提供C API和COM接口)。

答案 4 :(得分:3)

首先,您的解决方案不能是Excel UDF(用户定义的函数)。在我们的手册中,我们给出以下定义:“Excel UDF用于在Excel中构建自定义函数,以便最终用户在公式中使用它们。”我不介意你提出更好的定义:)

该定义显示UDF无法向UI添加按钮(我知道XLL可以修改CommandBar UI)或拦截键盘快捷方式以及Excel事件。

也就是说,ExcelDNA超出了范围,因为它用于开发XLL加载项。这同样适用于Add-in Express的Excel目标功能,因为它允许开发XLL加载项和Excel Automation加载项。

因为您需要处理Excel事件,所以您的解决方案可以是独立的应用程序,但这种方法存在明显的局限性。唯一真正的方法是创建一个COM加载项;它允许处理Excel事件并将自定义内容添加到Excel UI。你有三种可能性:

  • VSTO
  • 加载项Express(COM加载项功能)
  • 共享加载项(请参阅VS中“新建项目”对话框中的相应项目)

如果谈论开发Excel COM加载项,上面的3个工具提供了不同的功能:视觉设计师,填充等。但我不认为他们在访问Excel对象模型的速度上有所不同。说,我不知道(也无法想象)为什么从默认AppDomain获取COM对象应该与从另一个AppDomain获取相同的COM对象不同。顺便说一下,您可以通过创建共享加载项然后使用COM Shim向导来填充它来检查填充是否会影响操作速度。

速度II。正如我昨天给你写的那样:“加快读取和写入一系列单元格的最好方法是创建一个Excel.Range类型的变量,引用该范围,然后从/向Value属性读取/写入数组变量。“但与Francesco所说的相反,我并不把这归因于VSTO;这是Excel对象模型的一个功能。

速度III。最快的Excel UDF是用本机C ++编写的,而不是用任何.NET语言编写的。我还没有比较ExcelDNA和Add-in Express生成的XLL加载项的速度;我不认为你会在这里找到任何实质性的差异。

总结一下。我确信你的方法是错误的:基于Add-in Express,VSTO或Shared Add-in的COM加载项应该以相同的速度读写Excel单元。如果有人反驳这一陈述,我将很高兴(真诚地)。

现在就你的其他问题。 VSTO不允许开发支持Office 2000-2010的COM加载项。它需要三个不同的代码库和至少两个版本的Visual Studio才能完全支持Office 2003-2010;您需要有强大的神经和一部分好运才能为Excel 2003部署基于VSTO的加载项。使用Add-in Express,您可以为所有具有单个代码库的Office版本创建COM加载项;加载项Express为您提供了一个安装项目,可以在Excel 2000-2010(32位和64位)中安装加载项; ClickOnce部署也在船上。

VSTO在一个区域击败了Add-in Express:它允许创建所谓的文档级加载项。想象一下包含一些.NET代码的工作簿或模板;但是,如果部署这样的事情是一场噩梦,我不会感到惊讶。

在Excel事件上。例如,MSDN中列出了所有Excel事件,请参阅Excel 2007 events

白俄罗斯(GMT + 2)的问候,

安德烈斯莫林 加载项快速团队负责人

答案 5 :(得分:0)

我已经使用VBA代码(宏)收集&amp;压缩数据,并在一次调用C#时获取此数据,反之亦然。这可能是最有效的方法。

使用C#,您将始终需要使用一些编组。使用VSTO或COM Interop,底层通信层(编组开销)是相同的。

在VBA(Visual Basic For Application)中,您可以直接在Excel中的对象上工作。因此,访问这些数据的速度总是更快。

但....一旦你拥有了C#中的数据,对这些数据的操作就会快得多。

如果您使用的是VB6或C ++,那么您还需要通过COM界面,并且您还将面临跨进程编组。

因此,您正在寻找一种方法来最小化跨进程调用和编组。