Можно ли в Windows с COM-сервером, созданным на C #, вернуть SAFEARRAY как для кода с ранней, так и для поздней привязки?

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

Введение

  1. Я пишу COM-сервер на C #.
  2. COM-сервер предназначен для использования в Excel VBA как в режимах раннего, так и позднего связывания.
  3. Мой камень преткновения состоит в том, как вернуть БЕЗОПАСНЫЙ РЕЙС созданных классов, который работает как в режиме раннего, так и в позднем связывании; Я получаю ошибки.
  4. I have done plenty of work on this (all day):
    • I have done some diagnostics and setup the debugger to shed light on the errors I get.
    • Я провел довольно исчерпывающий поиск в Google.
    • Я нашел несколько менее чем удовлетворительных решений.
    • Я сейчас искренне озадачен и ищу специалиста по COM-взаимодействию, который поможет мне найти хорошее решение.

Для настройки типа проекта и свойств проекта

  1. Создайте новый проект библиотеки классов C #.
  2. Я назвал свой LateBoundSafeArraysProblem, а также переименовал исходный файл в LateBoundSafeArraysProblem.cs.
  3. В AssemblyInfo.cs измените строку 20 на ComVisible (true), чтобы видимость была универсальной (по-прежнему нужны общедоступные ключевые слова).
  4. Set the Project Properties:
    • Set the build options, in Project Properties->Build->Output I check the 'Register for COM interop' checkbox.
    • Set the debug options to launch Excel and load an excel workbook client:
      • In Project Properties->Debug->Start Action and select radio button 'Start external problem' and enter path to Microsoft Excel which for me is 'C:\Program Files\Microsoft Office 15\root\office15\excel.exe'.
      • В Project Properties-> Debug-> Start Options введите имя клиентской книги с поддержкой макросов Excel, которая для меня C: \ Temp \ LateBoundSafeArraysProblemClient.xlsm. †

Чтобы создать исходный код COM-сервера

  1. Style choices and decisions
    • I'm being a good COM citizen and dividing the interface definitions from the class definitions.
      • I'm using [ClassInterface(ClassInterfaceType.None)] and [ComDefaultInterface(typeof(<interface>))] attributes on the class to effect this clear division.
    • Поскольку клиент - это Excel VBA, нам нужно придерживаться типов, совместимых с автоматизацией, поэтому SAFEARRAY
  2. The two C# classes/ com classes:
    • Apples is a simple state vessel for marshalling data back to client and carries no methods except getters and setters.
    • FruitCounter - это рабочий класс, у которого есть метод enumerateApples (), который должен возвращать SAFEARRAY экземпляров Apples.

Итак, исходный код интерфейса и класса Apples:

public interface IApples 
{
    string variety { get; set; }
    int quantity { get; set; }
}

[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IApples))]
public class Apples : IApples
{
    public string variety { get; set; }
    public int quantity { get; set; }
}

Приведенный выше код не противоречит и работает нормально.

Исходный код интерфейса и класса FruitContainer:

public interface IFruitCounter
{
    Apples[] enumerateApples();
}

[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IFruitCounter))]
public class FruitCounter : IFruitCounter
{
    public Apples[] enumerateApples()
    {
        List<Apples> applesList = new List<Apples>();

        //* Add some apples - well, one in fact for the time being 
        Apples app = new Apples();
        app.variety = "Braeburn";
        app.quantity = 4;
        applesList.Add(app);

        // * finished adding apples want to convert to SAFEARRAY 
        return applesList.ToArray();
    }
}

И это будет работать для раннего связывания, но не для позднего связывания.

  1. Build project, one should have a dll and a tlb.
    • One output will be LateBoundSafeArraysProblem.dll
    • также будет выведена библиотека типов LateBoundSafeArraysProblem.tlb ‡

Клиентский код VBA для Excel с ранней привязкой

  1. Откройте книгу, указанную в параметрах запуска отладки (см. Выше †)
  2. Добавьте базовый стандартный модуль (не модуль класса).
  3. Перейдите в Инструменты-> Ссылки и установите флажок для созданной библиотеки типов (см. Выше ‡)
  4. Добавьте следующий код
    Sub TestEarlyBound()
        'Tools -> References to type library LateBoundSafeArraysProblem.tlb 
        Dim fc As LateBoundSafeArraysProblem.FruitCounter
        Set fc = New LateBoundSafeArraysProblem.FruitCounter

        Dim apples() As LateBoundSafeArraysProblem.apples
        apples() = fc.enumerateApples()

        Stop

    End Sub

Когда выполнение достигло остановки, можно проверить содержимое массива на предмет успешного маршалинга. УСПЕХ ДЛЯ РАННЕЙ ОБЯЗЫВКИ!

Клиентский код VBA для Excel с поздней привязкой

  1. Я также использую позднюю привязку в Excel VBA, чтобы избавиться от проблем с развертыванием, также я могу выполнять горячую замену dll, то есть устанавливать новый COM-сервер, не закрывая Excel (я должен опубликовать этот трюк на SO).
  2. Из (1) я собираюсь использовать ту же книгу Excel VBA в качестве платформы для тестирования позднего связывания и соответствующим образом изменить объявления.
  3. В том же модуле добавьте следующий код

        Sub TestFruitLateBound0()
    
    
        Dim fc As Object 'LateBoundSafeArraysProblem.FruitCounter
        Set fc = CreateObject("LateBoundSafeArraysProblem.FruitCounter")
    
        Dim apples() As Object 'LateBoundSafeArraysProblem.apples
        apples() = fc.enumerateApples()   '<==== Type Mismatch thrown
    
        Stop
    
    End Sub
    

Выполнение этого кода вызывает несоответствие типов (ошибка VB 13) в отмеченной строке. Таким образом, тот же код COM-сервера не работает в режиме позднего связывания Excel VBA. ОТКАЗ ЗА ПОЗДНУЮ БИНОВКУ!

Обходные пути

  1. Итак, во время расследования я написал второй метод с возвращаемым типом Object [], изначально это не сработало, потому что сгенерированный idl был сброшен как звездочка. Холостой ход пошел с

        // Return type Apples[] works for early binding but not late binding
        // works
        HRESULT enumerateApples([out, retval] SAFEARRAY(IApples*)* pRetVal);
    

    to

        // Return type Object[] fails because we drop a level of indirection 
        // (perhaps confusion between value and reference types)
        // does NOT work AT ALL (late or early)
        HRESULT enumerateApplesLateBound([out, retval] SAFEARRAY(VARIANT)* pRetVal);      // dropped as asterisk becomes SAFEARRAY to value types, no good 
    
  2. Использование атрибута MarshalAs исправило количество звездочек

        // Still with Object[] but using MarshalAs
        // [return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = System.Runtime.InteropServices.VarEnum.VT_UNKNOWN)]
        // works for late-bound but not early bound !!! Aaaargh !!!!
        HRESULT enumerateApplesLateBound([out, retval] SAFEARRAY(IDispatch*)* pRetVal);   
    

    и это сработало для позднего связывания, но НЕ для связывания эрлинга! Аааааааааааааааааааааааа!

Резюме

У меня работают два метода: один для раннего связывания и один для позднего связывания, что неудовлетворительно, потому что это означает удвоение каждого метода.

Как заставить работать один метод как для раннего, так и для позднего связывания?


person S Meaden    schedule 02.08.2016    source источник
comment
Похоже, у вас есть ошибка в коде VBA, вы должны использовать ключевое слово Set, чтобы назначить переменную apples. Без набора он попытается правильно восстановить значение по умолчанию. В массиве есть индексатор. Следом идут кошки и собаки. Не совсем уверен, почему доходит до хорошего конца ранней привязки, по-видимому, компилятор VBA знает, что не следует использовать свойство по умолчанию, когда видит, что это массив. Рассмотрите возможность фактического возврата перечислителя (System.Collections.IEnumerator) вместо массива так что вы можете использовать For Each.   -  person Hans Passant    schedule 02.08.2016
comment
Привет, Ганс, синтаксис VBA в порядке. Только что подтвердили код для классов в книге.   -  person S Meaden    schedule 02.08.2016
comment
Любопытно, что Dim apples As Variant: apples = fc.enumerateApples() работает при позднем связывании. Он даже возвращает True с Debug.Print IsArray(apples). Но тогда Debug.Print apples(0).variety - это ошибка 424: Требуется объект.   -  person Comintern    schedule 02.08.2016
comment
@ComIntern: А! Если это правда, это означает, что создается массив VARIANT, а не SAFEARRAY. Массив VARIANT ограничен примитивами. IUnknown и IDispatch. Хммм, как такое могло быть? Idl очень ясно говорит БЕЗОПАСНОСТЬ.   -  person S Meaden    schedule 02.08.2016
comment
Я действительно подумал об этом и попытался вернуть IApples[], но получил ту же ошибку. Он также должен объявлять IApples как IDispatch в tlb.   -  person Comintern    schedule 02.08.2016
comment
@ComIntern: Итак, мы используем (i) Dim apples as variant, а затем (ii) Object[] в качестве возвращаемого типа и (iii) MarshallAs. Я думаю, что это достаточно близко, чтобы быть решением. Строка кода может быть написана с помощью intellisense в сценарии раннего связывания и работает без изменений в сценарии позднего связывания. Мы просто будем думать о as Variant как о новом ключевом слове auto VBA (шутка C ++ 11) :). Если вы разместите в ответе, я награжу баллами. P.S. Он действительно говорит Object() в обозревателе объектов.   -  person S Meaden    schedule 02.08.2016


Ответы (1)


Я немного протестировал это, маршалируя возвращаемое значение в Variant, а затем сбросил память возвращенной структуры VARIANT, чтобы увидеть, что такое VARTYPE. Для вызова с ранней привязкой он возвращал Variant с VARTYPE VT_ARRAY & VT_DISPATCH. Для вызова с поздним связыванием он возвращал VARTYPE VT_ARRAY & VT_UNKNOWN. Яблоки должны уже быть определены как реализующие IDispatch в tlb, но по какой-то причине, которая ускользает от меня, VBA испытывает трудности с обработкой массива IUnknown из вызова с поздним связыванием. Обходной путь - изменить тип возвращаемого значения на object[] на стороне C # ...

public object[] enumerateApples()
{
    List<object> applesList = new List<object>();

    //* Add some apples - well, one in fact for the time being 
    Apples app = new Apples();
    app.variety = "Braeburn";
    app.quantity = 4;
    applesList.Add(app);

    // * finished adding apples want to convert to SAFEARRAY 
    return applesList.ToArray();
}

... и перетащите их в Variant на стороне VBA:

Sub TestEarlyBound()
    'Tools -> References to type library LateBoundSafeArraysProblem.tlb
    Dim fc As LateBoundSafeArraysProblem.FruitCounter
    Set fc = New LateBoundSafeArraysProblem.FruitCounter

    Dim apples As Variant
    apples = fc.enumerateApples()

    Debug.Print apples(0).variety   'prints "Braeburn"
End Sub

Sub TestFruitLateBound0()
    Dim fc As Object
    Set fc = CreateObject("LateBoundSafeArraysProblem.FruitCounter")

    Dim apples As Variant
    apples = fc.enumerateApples()

    Debug.Print apples(0).variety   'prints "Braeburn"
End Sub
person Comintern    schedule 02.08.2016
comment
Здорово! Бесконечно благодарен. - person S Meaden; 02.08.2016