Вопрос довольно длинный, поэтому для облегчения обсуждения я выделю маркированный список.
Введение
- Я пишу COM-сервер на C #.
- COM-сервер предназначен для использования в Excel VBA как в режимах раннего, так и позднего связывания.
- Мой камень преткновения состоит в том, как вернуть БЕЗОПАСНЫЙ РЕЙС созданных классов, который работает как в режиме раннего, так и в позднем связывании; Я получаю ошибки.
- 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-взаимодействию, который поможет мне найти хорошее решение.
Для настройки типа проекта и свойств проекта
- Создайте новый проект библиотеки классов C #.
- Я назвал свой LateBoundSafeArraysProblem, а также переименовал исходный файл в LateBoundSafeArraysProblem.cs.
- В AssemblyInfo.cs измените строку 20 на ComVisible (true), чтобы видимость была универсальной (по-прежнему нужны общедоступные ключевые слова).
- 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-сервера
- 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
- I'm being a good COM citizen and dividing the interface definitions from the class definitions.
- 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();
}
}
И это будет работать для раннего связывания, но не для позднего связывания.
- Build project, one should have a dll and a tlb.
- One output will be LateBoundSafeArraysProblem.dll
- также будет выведена библиотека типов LateBoundSafeArraysProblem.tlb ‡
Клиентский код VBA для Excel с ранней привязкой
- Откройте книгу, указанную в параметрах запуска отладки (см. Выше †)
- Добавьте базовый стандартный модуль (не модуль класса).
- Перейдите в Инструменты-> Ссылки и установите флажок для созданной библиотеки типов (см. Выше ‡)
- Добавьте следующий код
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 с поздней привязкой
- Я также использую позднюю привязку в Excel VBA, чтобы избавиться от проблем с развертыванием, также я могу выполнять горячую замену dll, то есть устанавливать новый COM-сервер, не закрывая Excel (я должен опубликовать этот трюк на SO).
- Из (1) я собираюсь использовать ту же книгу Excel VBA в качестве платформы для тестирования позднего связывания и соответствующим образом изменить объявления.
В том же модуле добавьте следующий код
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. ОТКАЗ ЗА ПОЗДНУЮ БИНОВКУ!
Обходные пути
Итак, во время расследования я написал второй метод с возвращаемым типом 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
Использование атрибута 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);
и это сработало для позднего связывания, но НЕ для связывания эрлинга! Аааааааааааааааааааааааа!
Резюме
У меня работают два метода: один для раннего связывания и один для позднего связывания, что неудовлетворительно, потому что это означает удвоение каждого метода.
Dim apples As Variant: apples = fc.enumerateApples()
работает при позднем связывании. Он даже возвращает True сDebug.Print IsArray(apples)
. Но тогдаDebug.Print apples(0).variety
- это ошибка 424: Требуется объект. - person Comintern   schedule 02.08.2016IApples[]
, но получил ту же ошибку. Он также должен объявлять IApples как IDispatch в tlb. - person Comintern   schedule 02.08.2016Dim apples as variant
, а затем (ii)Object[]
в качестве возвращаемого типа и (iii)MarshallAs
. Я думаю, что это достаточно близко, чтобы быть решением. Строка кода может быть написана с помощью intellisense в сценарии раннего связывания и работает без изменений в сценарии позднего связывания. Мы просто будем думать оas Variant
как о новом ключевом словеauto
VBA (шутка C ++ 11) :). Если вы разместите в ответе, я награжу баллами. P.S. Он действительно говоритObject()
в обозревателе объектов. - person S Meaden   schedule 02.08.2016