Excel UDF计算应返回“原始”值

时间:2010-12-29 16:25:01

标签: excel function recursion user-defined-functions cyclic

我一直在努力解决VBA问题,我会尽量彻底解释它。

我已经使用自己的RTD实现创建了一个VSTO插件,我正在从Excel工作表中调用它。为了避免在单元格中使用完整的RTD语法,我创建了一个从工作表中隐藏该API的UDF。 我创建的RTD服务器可以通过自定义功能区组件中的按钮启用和禁用。

我想要实现的行为如下:

  • 如果服务器已禁用并且在单元格中输入了对我的功能的引用,我希望单元格显示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上都很难。为了允许用户自己决定他是否希望在他的机器上加载,他或她可以从服务器禁用更新。在这种情况下,他或她应该仍然能够使用当前字段中的值来计算工作表,但不会将更新推送到这些工作表中。一旦需要新数据,就可以启用服务器并更新字段。

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

3 个答案:

答案 0 :(得分:4)

不同的方法=新答案。

我发现了一些困难的事情,你可能觉得有用:

<强> 1 在UDF中,像这样返回RTD调用

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

表现得好像是在单元格中插入了注释函数,而不是RTD返回的值。换句话说,“结果”是“RTD-function-call”类型的对象,而不是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 Addin是件小事......直到你真正尝试构建一个工业化,可分发的解决方案。获得安装程序的所有权限和权限是一场噩梦,如果您有支持多个版本的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元素字典(整数,整数)中的值比从Excel中获取单元格值快<11>快

Q.E.D。

答案 1 :(得分:0)

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

当你启用服务器时,这可能是一个问题,你必须欺骗Excel再次调用你的UDF,这取决于你想要做什么。

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

答案 2 :(得分:0)

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