【问题标题】:Excel UDF calculation should return 'original' valueExcel UDF 计算应返回“原始”值
【发布时间】:2011-06-01 03:31:55
【问题描述】:

我用我自己的 RTD 实现创建了一个 VSTO 插件,我从我的 Excel 表中调用它。为了避免必须在单元格中使用成熟的 RTD 语法,我创建了一个 UDF,该 UDF 将该 API 从工作表中隐藏起来。 我创建的 RTD 服务器可以通过自定义 Ribbon 组件中的按钮启用和禁用。

我想要实现的行为如下:

  • 如果服务器禁用并且在单元格中输入了对我的函数的引用,我希望单元格显示Disabled
  • 如果服务器已禁用,但该功能已在启用时输入到单元格中(因此单元格会显示一个值),我希望该单元格继续显示该值。
  • 如果服务器启用,我希望单元格显示Loading

听起来很简单。这是一个非功能性代码的示例:

Public Function RetrieveData(id as Long)
  Dim result as String

  // This returns either 'Disabled' or 'Loading'
  result = Application.Worksheet.Function.RTD("SERVERNAME", "", id)
  RetrieveData = result

  If(result = "Disabled") Then

    // Obviously, this recurses (and fails), so that's not an option
    If(Not IsEmpty(Application.Caller.Value2)) Then

      // So does this
      RetrieveData = Application.Caller.Value2

    End If
  End If
End Function

该函数将在数千个单元格中调用,因此将“原始”值存储在另一个数据结构中将是一个主要开销,我想避免它。此外,RTD 服务器不知道这些值,因为它也不保留它的历史记录,或多或少是出于相同的原因。

我在想可能有某种方法可以退出该函数,这会迫使它不改变显示的值,但到目前为止我还没有找到类似的东西。

编辑:
由于大众的需求,一些关于我为什么要做这一切的额外信息: 正如我所说,该函数将在数千个单元中调用,RTD 服务器需要检索相当多的信息。这在网络和 CPU 上都非常困难。为了让用户自己决定是否要在他的机器上加载这个负载,他们可以禁用来自服务器的更新。在这种情况下,他们应该仍然能够使用当前字段中的值计算工作表,但不会向其中推送任何更新。一旦需要新数据,就可以启用服务器并更新字段。

再一次,由于我们在这里讨论了相当多的数据,我宁愿不要将它存储在工作表中的某个位置。此外,即使工作簿关闭并再次加载,数据也应该可用。

【问题讨论】:

  • 其他人将不得不提供有关使用 Excel 的替代设计的真实答案,但是一旦 UDF 被触发,就无法让单元格单独离开,也没有办法让UDF 知道调用它的单元格的“先前”值(不将其存储在其他地方在触发计算之前)。这只是电子表格工作方式的基础。
  • @jtolle 不过很遗憾:假设您想计算单元格中的计算迭代次数。为此,您总是需要第二个数据结构或另一个单元格来跟踪计数器。在我看来,这是 Excel 的设计缺陷……
  • 为什么不把你的'retrieveData'东西从一个UDF中拉出来,放到一个普通的宏中呢?然后你可以根据你想要的任何逻辑覆盖或不覆盖单元格。您仍然可以拥有指向这些单元格的公式,并且那些可以在检索到的数据(或任何其他输入)发生变化时以正常的电子表格方式重新计算。
  • 我也在考虑这个问题,但我真的希望用户能够使用一个函数——从 API 的角度来看它更有意义,而且他们做错事的可能性很大更小。

标签: excel vba function user-defined-functions


【解决方案1】:

不同的策略=新的答案。

我发现了一些你可能会觉得有用的东西:

1. 在 UDF 中,像这样返回 RTD 调用

' excel equivalent: =RTD("GeodesiX.RTD",,"status","Tokyo")
result = excel.WorksheetFunction.rtd( _
    "GeodesiX.RTD", _
    Nothing, _
    "geocode", _
    request, _
    location)

表现得好像您在单元格中插入了注释函数,而不是 RTD 返回的值。换句话说,“结果”是“RTD 函数调用”类型的对象,而不是 RTD 的答案。相反,这样做:

' excel equivalent: =RTD("GeodesiX.RTD",,"status","Tokyo")
result = excel.WorksheetFunction.rtd( _
    "GeodesiX.RTD", _
    Nothing, _
    "geocode", _
    request, _
    location).ToDouble ' or ToString or whetever

返回实际值,相当于在单元格中输入“3.1418”。这是一个重要的区别。在第一种情况下,细胞继续参与 RTD 馈送,在第二种情况下,它只是获得一个恒定值。这可能是您的解决方案。

2. MS VSTO 让编写 Office 插件看起来就像小菜一碟……直到您真正尝试构建一个工业的、可分发的解决方案。为安装程序获得所有权限和权限是一场噩梦,如果您有支持多个 Excel 版本的好主意,情况会变得更糟。我已经使用Addin Express 好几年了。它隐藏了所有这些 MS 讨厌的东西,让我专注于编写我的插件。他们的支持也是一流的,值得一看。 (不,我没有附属或类似的东西)。

3. 请注意,Excel 可以并且会在任何时候调用 Connect / RefreshData / RTD,即使您正在处理某些事情 - 幕后正在进行一些微妙的多任务处理。您需要使用适当的 Synclock 块来装饰您的代码,以保护您的数据结构。

4. 当您收到数据(可能是在单独的线程上异步)时,您绝对必须在最初调用您的线程(通过 Excel)上回调 Excel。如果你不这样做,它会在一段时间内正常工作,然后你会开始出现神秘的、无法解决的崩溃,更糟糕的是,后台出现孤立的 Excel。以下是执行此操作的相关代码示例:

    Imports System.Threading
    ...
    Private _Context As SynchronizationContext = Nothing
    ...
    Sub New
      _Context = SynchronizationContext.Current
      If _Context Is Nothing Then
         _Context = New SynchronizationContext ' try valiantly to continue    
      End If
    ...
    Private Delegate Sub CallBackDelegate(ByVal GeodesicCompleted)

    Private Sub GeodesicComplete(ByVal query As Query) _
        Handles geodesic.Completed ' Called by asynchronous thread

        Dim cbd As New CallBackDelegate(AddressOf GeodesicCompleted)

        _Context.Post(Function() cbd.DynamicInvoke(query), Nothing)
    End Sub
    Private Sub GeodesicCompleted(ByVal query As Query)

        SyncLock query

            If query.Status = "OK" Then

                Select Case query.Type

                    Case Geodesics.Query.QueryType.Directions
                        GeodesicCompletedTravel(query)

                    Case Geodesics.Query.QueryType.Geocode
                        GeodesicCompletedGeocode(query)

                End Select
            End If

            ' If it's not resolved, it stays "queued", 
            ' so as never to enter the queue again in this session
            query.Queued = Not query.Resolved

        End SyncLock

        For Each topic As AddinExpress.RTD.ADXRTDTopic In query.Topics
            AddinExpress.RTD.ADXRTDServerModule.CurrentInstance.UpdateTopic(topic)
        Next

    End Sub

5. 我所做的事情显然类似于您在this addin 中提出的问题。在那里,我从 Google 异步获取地理编码数据,并使用由 UDF 遮蔽的 RTD 提供它。由于调用 GoogleMaps 非常昂贵,我尝试了 101 种方法和几个月的晚上来保持单元格中的价值,就像你正在尝试的那样,但没有成功。我没有计时,但我的直觉是,像“Application.Caller.Value”这样对 Excel 的调用比字典查找慢一个数量级。

最后,我创建了一个缓存组件,它保存并重新加载已经从我在 Workbook OnSave 中动态创建的非常隐藏的电子表格中获得的值。数据存储在 Dictionary(of string, myQuery) 中,其中每个 myQuery 保存所有相关信息。

它运行良好,满足离线工作的要求,甚至对于 20'000 多个公式,它看起来都是瞬时的。

HTH。


编辑:出于好奇,我测试了调用 Excel 比查找字典要贵得多的预感。事实证明,这种预感不仅是正确的,而且令人恐惧。

Public Sub TimeTest()
    Dim sw As New Stopwatch
    Dim row As Integer
    Dim val As Object
    Dim sheet As Microsoft.Office.Interop.Excel.Worksheet
    Dim dict As New Dictionary(Of Integer, Integer)

    Const iterations As Integer = 100000
    Const elements As Integer = 10000

    For i = 1 To elements + 1
        dict.Add(i, i)
    Next
    sheet = _ExcelWorkbook.ActiveSheet

    sw.Reset()
    sw.Start()
    For i As Integer = 1 To iterations
        row = 1 + Rnd() * elements
    Next
    sw.Stop()
    Debug.WriteLine("Empty loop     " & (sw.ElapsedMilliseconds * 1000) / iterations & " uS")

    sw.Reset()
    sw.Start()
    For i As Integer = 1 To iterations
        row = 1 + Rnd() * elements
        val = sheet.Cells(row, 1).value
    Next
    sw.Stop()
    Debug.WriteLine("Get cell value " & (sw.ElapsedMilliseconds * 1000) / iterations & " uS")

    sw.Reset()
    sw.Start()
    For i As Integer = 1 To iterations
        row = 1 + Rnd() * elements
        val = dict(row)
    Next
    sw.Stop()
    Debug.WriteLine("Get dict value " & (sw.ElapsedMilliseconds * 1000) / iterations & " uS")

End Sub

结果:

Empty loop     0.07 uS
Get cell value 899.77 uS
Get dict value 0.15 uS

在 10'000 个元素的 Dictionary(Of Integer, Integer) 中查找值比从 Excel 中获取单元格值快 超过 11'000 倍

Q.E.D.

【讨论】:

  • 11000因素是使用Interop造成的。如果您使用 COM VBA,该系数约为 5,而对于 XLL,它会更低。但是字典很难被击败。
  • @charles 有趣的评论,谢谢。我将如何从 .NET 项目中使用 COM VBA?
  • @smirkingman 这是迄今为止我收到的问题的最广泛的答案之一 - 非常感谢您抽出时间和精力来分享您的知识。上周我一直在尝试不同的方法,最终得到了与您提出的几乎相同的架构:我编写了一个简单的缓存。这不是最佳解决方案,但到目前为止它可以工作并且不会消耗太多内存。再次,非常感谢:我会将您的答案标记为已接受。
  • @LeChe 不客气,很高兴能为您提供帮助。我很快就会开源我的 geodesix 插件以及文档。如果您有兴趣,请在 CALVERT dot CH 给我留言 MAURICE
  • @smirkingman 不幸的是,将 .NET 与 Excel 连接的唯一有效方法是通过 XLL 接口。您可以使用 Addin Express、Excel DNA 或托管 XLL 执行此操作。否则(遗憾的是)使用 VBA 比使用 .Net 更有效:希望这会改变一段时间。
【解决方案2】:

也许...尝试使您的 UDF 包装函数为非易失性,这样它就不会被调用,除非它的参数之一发生变化。

这可能是您启用服务器时出现的问题,您必须欺骗 Excel 再次调用您的 UDF,这取决于您要执行的操作。

也许解释一下您要实现的完整功能?

【讨论】:

  • 感谢您的指点,我将看看非易失性函数。事实上,我只希望在其中一个参数发生变化时进行计算。另外,我希望活动的 RTD 服务器仍然会触发单元更新,即使调用 RTD 的函数是非易失性的。顺便说一句,我上面描述的是完整的功能 - 真的没有魔法,它只是我的 RTD 调用的包装器(加上我正在努力解决的一些额外的事情:)。无论如何,我会在这个问题上添加更多细节,说明我为什么要做这一切......
  • 原来你不能将函数标记为非易失性的,只能是易失性的。所以默认情况下,它们是非易失性的。太糟糕了,似乎这不是要走的路……:(
【解决方案3】:

您可以尝试 Application.Caller.Text
这具有从渲染层返回格式化值作为文本的缺点,但似乎避免了循环引用问题。
注意:我没有测试过这个 hack在所有可能的情况下...

【讨论】:

  • 你打败了我。 :) 我也在玩 .Text 属性。然而正如你所说:它返回格式化的值,这不是真正的值。另外——尤其是在金融环境中——这可能会引入非常糟糕的错误。但是非常感谢您的意见,这似乎确实是唯一的选择。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多