При вычислении Excel UDF должно возвращаться «оригинальное» значение - PullRequest
2 голосов
/ 29 декабря 2010

Я некоторое время боролся с проблемой VBA и постараюсь объяснить ее как можно более подробно.

Я создал плагин VSTO со своей собственной реализацией RTD, которую я вызываю из своих листов Excel. Чтобы избежать необходимости использовать полноценный RTD-синтаксис в ячейках, я создал UDF, который скрывает этот API-интерфейс от листа. Созданный мной 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-сервер не знает значений, поскольку он также не хранит историю этого, более или менее по той же причине.

Я думал, что мог бы быть какой-то способ выйти из функции, который заставил бы ее не изменять отображаемое значение, но до сих пор я не смог найти ничего подобного.

Любые идеи о том, как решить эту проблему, приветствуются!

Спасибо, Che

EDIT:
Из-за широкого спроса, дополнительная информация о том, почему я хочу сделать все это: Как я уже сказал, функция будет вызываться в тысячах ячеек, и RTD-сервер должен получать довольно много информации. Это может быть довольно сложно как для сети, так и для процессора. Чтобы позволить пользователю решить для себя, хочет ли он эту загрузку на своем компьютере, он или она может отключить обновления с сервера. В этом случае он или она все еще должны иметь возможность вычислять листы со значениями, находящимися в настоящее время в полях, но в них не помещаются обновления. Как только новые данные требуются, сервер может быть включен, и поля будут обновлены.

Опять же, поскольку здесь речь идет о довольно большом количестве данных, я бы не стал хранить их где-то на листе. Кроме того, данные должны использоваться, даже если рабочая книга закрыта и загружена снова.

Ответы [ 3 ]

4 голосов
/ 31 декабря 2010

Другой курс = новый ответ.

Несколько трудностей, которые я открыл для себя, которые могут оказаться полезными:

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 Addin - это очень просто ... до тех пор, пока вы на самом деле не попытаетесь создать промышленный продукт,распространяемое решение.Получение всех привилегий и полномочий для установки - это кошмар, и оно значительно ухудшается, если у вас есть блестящая идея поддержки более чем одной версии Excel.Я пользуюсь Addin Express уже несколько лет.Это скрывает всю эту злобу MS и позвольте мне сосредоточиться на кодировании моего аддина.Их поддержка тоже первоклассная, стоит посмотреть.(Нет, я не аффилирован или что-то в этом роде.)

3. Помните, что Excel может и будет вызывать Connect / RefreshData / RTD в любое время, даже когда вы находитесь всередина чего-то - за кулисами происходит какая-то тонкая многозадачность.Вам нужно будет украсить свой код соответствующими блоками Synclock, чтобы защитить свои структуры данных.

4. Когда вы получаете данные (предположительно асинхронно в отдельном потоке), вы абсолютноНЕОБХОДИМО обратный вызов Excel в потоке, в котором вы изначально были вызваны (Excel).Если вы этого не сделаете, какое-то время он будет работать нормально, а затем вы начнете получать таинственные, неразрешимые сбои и, что еще хуже, осиротелые Excels на фоне.Вот пример соответствующего кода для этого:

    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. Я сделал что-то вроде того, что вы просите в этом дополнении ,Там я асинхронно извлекаю данные геокодирования из Google и подаю их с RTD, скрытым UDF.Поскольку звонок в GoogleMaps очень дорогой, я попробовал 101 способ и несколько месяцев по вечерам, чтобы сохранить значение в ячейке, как то, что вы пытаетесь, безуспешно.Я ничего не рассчитывал, но мое внутреннее чувство таково, что вызов Excel, такой как «Application.Caller.Value», на порядок медленнее, чем поиск по словарю.

В конце я создал компонент кэшакоторый сохраняет и повторно загружает значения, уже полученные из очень скрытой электронной таблицы, которую я создаю на лету в Workbook OnSave.Данные хранятся в словаре (строки, 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 элементов (Of Integer,Integer) на в 11000 раз быстрее , чем выборка значения ячейки из Excel.

QED

0 голосов
/ 30 декабря 2010

Вы можете попробовать Application.Caller.Text
. Этот недостаток заключается в возврате отформатированного значения из слоя рендеринга в виде текста, но, похоже, позволяет избежать проблемы циклической ссылки.
Примечание. этот взлом при всех возможных обстоятельствах ...

0 голосов
/ 30 декабря 2010

Может быть ... Попробуйте сделать вашу функцию-оболочку UDF энергонезависимой, чтобы она не вызывалась, пока не изменился один из ее аргументов.

Это может быть проблемой, когда вы включаете сервер, вам придется заставить Excel снова вызывать вашу UDF, это зависит от того, что вы пытаетесь сделать.

Возможно, объясните полную функцию, которую вы пытаетесь реализовать?

...