Сообщений 4 Оценка 15 [+0/-1] Оценить |
Несмотря на явные преимущества управляемого кода .Net бывают ситуации, когда при построении программных систем приходится заниматься разработкой COM-объектов. Так, например, для организации пользовательских интерфейсов в системах уровня предприятия нередко используются средства Microsoft Office, функциональные возможности которых расширяются за счет макросов (программ), реализуемых с помощью VBA (Visual Basic for Applications). Возможности же VBA в свою очередь расширяются именно за счет использования COM-объектов.
ПРИМЕЧАНИЕ Здесь имеются в виду версии Office, которые напрямую не поддерживают .Net, т.е. до версии Microsoft Office 2003 SP1. |
Другая ситуация, в которой приходится использовать COM не по желанию, а по необходимости – разработка дополнительной функциональности для существующих программ, которые расширяются за счет COM. Примеры подобных программ – Microsoft Outlook, Microsoft Internet Explorer и др.
Возможны и другие ситуации, когда без COM не обойтись – например, реализация с помощью .Net существующих COM-протоколов. Частный пример – разработка OPC-серверов для систем сбора информации (OPC – OLE for Process Control).
Таким образом, хотя разработка COM-объектов и не является главной возможностью .Net, владение этой техникой может быть весьма полезным.
В настоящей статье не будет рассматриваться C++ with managed extensions – разработка COM-объектов с помощью этого языка программирования не очень существенно отличается от традиционной разработки с помощью C++. Ограничимся лишь C#, который позволяет разрабатывать COM-объекты гораздо проще, чем C++ или Delphi (субъективное мнение автора, основанное на личном практическом опыте).
Более того, не будут здесь рассматриваться ни общие вопросы COM, ни конкретные механизмы и технологии C# для разработки COM – все это достаточно подробно документировано в MSDN. Будет рассмотрен абсолютно частный вопрос, который, к сожалению, не нашел отражения ни в MSDN, ни в других источниках информации, доступных автору, включая поиск в Сети – как средствами C# реализовать COM-коллекции.
Руководство по программированию на Visual Basic для Microsoft Excel 5.0 определяет термин Automation collection следующим образом:
«A group of objects. An object's position in the collection can change whenever a change occurs in the collection. Therefore, the position of any specific object in the collection is unpredictable. This unpredictability distinguishes a collection from an array.»
Более детальное определение COM-коллекции включает следующие моменты:
С точки зрения внешнего кода (например, VBA) использовать COM-коллекции можно двумя способами.
Первый способ заключается в использовании обычного цикла for, например:
For I = 1 To collection.Count Set element = collection.Item(I) ' некоторые действия с элементом коллекции Next I |
или с учетом доступа к значению по умолчанию:
For I = 1 To collection.Count Set element = collection(I) ' некоторые действия с элементом коллекции Next I |
Второй способ заключается в использовании цикла типа for each (поддерживается не всеми языками программирования):
For Each element In collection ' некоторые действия с элементом коллекции Next |
Для упрощения кода примем следующие ограничения для разрабатываемых коллекций:
Примем также единую нумерацию элементов всех коллекций, начиная с 1.
COM-интерфейс IEnumVARIANT определен в библиотеке stdole32.dll, которая находится в каталоге <Windows>\system32. Указанная библиотека сопровождается файлом stdole32.tlb, что позволяет импортировать интерфейс IEnumVARIANT в .Net с помощью утилиты tlbimp.exe из состава .Net Framework SDK. Результат импорта – сборка, аналогичная сборке stdole.dll из поставки Microsoft .NET Primary Interop Assemblies (PIA).
К сожалению, импортированный таким образом интерфейс оказывается неработоспособным при обратной передаче в COM.
PIA-сборку можно построить и вручную, но это занятие хлопотное и к тому же неблагодарное, т.к. пространство имен System.Runtime.InteropServices уже включает определение интерфейса UCOMIEnumVARIANT, соответствующего IEnumVARIANT.
Назначение и рекомендации по реализации всех методов интерфейса IEnumVARIANT хорошо документированы в MSDN. Отметим лишь, что этот интерфейс в разном программном окружении может использоваться с некоторыми вариациями. Так, Visual Basic, VBA, VBScript используют следующую схему работы с интерфейсом (псевдокод C#):
IEnumVARIANT enumerator = collection._NewEnum(); while (true) { object element; int pceltFetched; int hr = enumerator.Next(1, out element, out pceltFetched); if (hr == 0) // некоторые действия с elementelsebreak; } |
Таким образом, если потенциальное использование интерфейса ограничить только указанными языками программирования, можно существенно упростить реализацию.
С целью же универсального использования различных .Net-коллекций (Array, ArrayList, etc.) можно использовать интерфейс System.Collections.IEnumerator, получаемый через интерфейс IEnumerable реальной коллекции.
С учетом изложенного общее определение класса, реализующего COM-интерфейс IEnumVARIANT, будет следующим (константы S_OK и S_FALSE будут использованы позже в реализации методов):
public class _EnumVariant : UCOMIEnumVARIANT { privateconstint S_OK = 0; privateconstint S_FALSE = 1; public _EnumVariant(IEnumerator enumerator) { this.enumerator = enumerator; } publicvoid Clone(int ppenum) { ... } publicint Next(int celt, int rgvar, int pceltFetched) { ... } publicint Reset(){ ... } publicint Skip(int celt) { ... } IEnumerator enumerator = null; } |
Отметим, что в классе _EnumVariant не предусматривается конструктор без параметров. Это позволяет исключить создание экземпляров нумератора во внешнем коде. Кроме того, имя класса начинается с подчеркивания, что в соответствии с рекомендациями Microsoft позволяет сделать его «невидимым» для Visual Basic, VBA, VBScript и пр.
Далее рассмотрим реализацию отдельных методов.
С учетом того, что разрабатываемый нумератор будет использоваться только в контексте Visual Basic, VBA, VBScript (см. Общие положения), метод можно не реализовывать:
public void Clone(int ppenum) { thrownew NotSupportedException(); } |
Если же в другом контексте этот метод необходим, то реализация класса изменится принципиально, т.к. клонирование через IEnumerator невозможно.
Кроме того, в соответствии с описанием интерфейса IEnumVARIANT через параметр ppenum придется возвращать указатель на новый нумератор, а сигнатура C#-метода на первый взгляд для этого не очень подходит. Впрочем, желаемого можно добиться и в такой ситуации (соответствующие решения см. в реализации метода Next).
В разрабатываемом контексте (см. Общие положения) метод Reset не является необходимым, но легко реализуется через IEnumerator:
public int Reset() { enumerator.Reset(); return S_OK; } |
Единственный нюанс реализации – возврат значения S_OK. Впрочем, если бы мы описывали подобный интерфейс вручную, то получили бы аналогичную ситуацию при использовании следующей конструкции:
[PreserveSig]
int Reset();
|
Реализация метода через IEnumerator также не представляет труда:
public int Skip(int celt) { for ( ; celt > 0; celt--) if (!enumerator.MoveNext()) return S_FALSE; return S_OK; } |
Отметим только, что в реализации этого метода предполагается значение параметра celt, большее 0. Это соответствует описанию метода IEnumVARIANT::Skip в MSDN.
Данный метод является по сути единственным значимым и достаточно нестандартным с точки зрения реализации. Поэтому разберем его подробнее.
Сравним сигнатуры методов IEnumVARIANT.Next (OLE) и UCOMIEnumVARIANT.Next (C#):
[OLE] HRESULT Next( unsignedlong celt, VARIANT FAR* rgVar, unsignedlong FAR* pCeltFetched ); [C#] publicint Next(int celt, int rgvar, int pceltFetched); |
Возвращаемое значение формируется аналогично методу Reset.
Параметр celt тоже не представляет проблем – в обоих случаях это обычный входной параметр.
С параметром pCeltFetched ситуация несколько сложнее. В соответствии с описанием метода IEnumVARIANT.Next через этот параметр передается указатель на переменную, в которую записывается количество считанных элементов или null, если внешнее окружение не использует эту информацию. Такая конструкция в C# не может быть определена ни через ref-, ни через out-параметр, поэтому тип этого параметра и определен как int (хотя, наверное, удобнее было бы использовать IntPtr).
Тем не менее, можно использовать класс Marshal, чтобы напрямую записать результат в неуправляемую память:
IntPtr pceltFetchedPtr = new IntPtr(pceltFetched); if (pceltFetchedPtr != IntPtr.Zero) Marshal.WriteInt32(pceltFetchedPtr, 0); |
Параметр rgVar создает еще большую проблему, т.к. через него внешний код передает массив (неинициализированных) значений типа OLE-variant емкостью celt, а реализация метода должна заполнить этот массив значениями считанных элементов коллекции.
Такую функциональность можно реализовать с помощью ручного маршалинга посредством класса Marshal и небольшого трюкачества с unsafe-кодом:
int* p = (int*)rgvar; for (int i = 0; i < celt; i++) { if (enumerator.MoveNext()) Marshal.GetNativeVariantForObject( enumerator.Current, new IntPtr((void*)p)); p += 4; } |
Трюк заключается в том, что для перехода к очередному элементу выходного массива нужно прибавить 4 к указателю на int,что и обеспечит смещение в 16 байт (размер OLE-variant).
Наверное, трюкачество с указателями – не самый лучший подход для управляемых языков. Впрочем, подобную технику можно встретить и в «промышленных» разработках, и даже в исходных текстах самого .Net Framework. А иначе зачем было вводить в язык возможности unsafe-кода?
Если же такие трюки кому-то не нравятся, можно описать структуру, соответствующую tagVARIANT из C++, и выполнить операции с указателем на эту структуру:
struct VARIANT { publicshort vt; publicshort reserved1; publicshort reserved2; publicshort reserved3; publicint data1; publicint data2; } //... VARIANT* p = (VARIANT*)rgvar; for (int i = 0; i < celt; i++, p++) if (enumerator.MoveNext()) Marshal.GetNativeVariantForObject( enumerator.Current, new IntPtr((void*)p)); |
Впрочем, вводить структуру только для того, чтобы выполнять корректное перемещение указателя – это уже в некотором смысле излишество. Поэтому оставим вариант с «трюком», и тогда полный текст метода Next будет выглядеть следующим образом:
public int Next(int celt, int rgvar, int pceltFetched) { IntPtr pceltFetchedPtr = new IntPtr(pceltFetched); if (pceltFetchedPtr != IntPtr.Zero) Marshal.WriteInt32(pceltFetchedPtr, 0); if (celt <= 0) return S_FALSE; else { unsafe { int* p = (int*)rgvar; for (int i = 0; i < celt; i++) { if (enumerator.MoveNext()) Marshal.GetNativeVariantForObject( enumerator.Current, new IntPtr((void*)p)); elsereturn S_FALSE; p += 4; // смещаем указатель на 16 байт вперед } } if (pceltFetchedPtr != IntPtr.Zero) Marshal.WriteInt32(pceltFetchedPtr, celt); return S_OK; } } |
С учетом же того, что Visual Basic, VBA, VBScript всегда считывают по одному элементу коллекции, исходный код метода можно существенно упростить:
public int Next(int celt, int rgvar, int pceltFetched) { if (celt == 1 && enumerator.MoveNext()) { Marshal.GetNativeVariantForObject( enumerator.Current, new IntPtr(rgvar)); return S_OK; } elsereturn S_FALSE; } |
При реализации коллекций необходимо использовать рекомендации Microsoft по разработке COM-объектов в .Net, сформулированные в документации .Net Framework SDK. В частности, целесообразно использовать такие атрибуты, как GuidAttribute, ClassInterfaceAttribute, ComVisibleAttribute и пр.
Кроме того, необходимо использовать явное определение интерфейса коллекции, подобное следующему:
public interface IxxxCollection { [DispId(1)] int Count { get; } [DispId(0)] object Item([In] int index); [DispId(-4)] object _NewEnum { [return: MarshalAs(UnmanagedType.IUnknown)] get; } } |
Значения идентификаторов DispId членов Item и _NewEnum важны.
Для метода Item идентификатор DispId должен иметь зарезервированное значение 0 (соответствует DISPID_VALUE), что позволяет внешнему окружению использовать этот метод неявно (пример VBA):
x = collection.Items(0) x = collection(0) |
Для свойства _NewEnum идентификатор DispId должен иметь зарезервированное значение -4 (соответствует DISPID_NEWENUM), что позволяет внешнему окружению использовать это свойство для получения нумератора. Кроме того, подчеркивание в начале названия свойства позволяет сделать это свойство «невидимым» для сред типа VB (в соответствии с рекомендациями Microsoft).
В качестве объектов нетипизированной коллекции используем COM-объекты Loan из примера, поставляемого с .Net Framework SDK (см. проект <SDK>\Samples\Technologies\Interop\Applications\LoanApps\COMtoNET\loanlib).
Исходный код:
public interface ILoanCollection { [DispId(1)] int Count { get; } [DispId(0)] object Item([In] int index); [DispId(-4)] object _NewEnum { [return: MarshalAs(UnmanagedType.IUnknown)] get; } } publicclass LoanCollection : ILoanCollection { public LoanCollection() { items = new Loan[2]; items[0] = new Loan(); items[0].OpeningBalance = 100; items[1] = new Loan(); items[1].OpeningBalance = 200; } publicint Count { get { return items.Length; } } publicobject Item(int index) { try { return items[index - 1]; } catch { returnnull; } } publicobject _NewEnum { get { returnnew _EnumVariant(items.GetEnumerator()); } } Loan[] items; } |
Типизированная коллекция реализуется аналогично нетипизированной (см. выше), за исключением того, что метод Item будет возвращать типизированное значение интерфейса ILoan:
public interface ITypedLoanCollection { //... [DispId(0)] ILoan Item([In] int index); //... } publicclass TypedLoanCollection : ITypedLoanCollection { //...public ILoan Item(int index) { try { return items[index - 1]; } catch { returnnull; } } //... Loan[] items; } |
Фактически метод Item может возвращать и объект Loan. По крайней мере, VBA одинаково корректно работает в обоих случаях.
Кроме того, в соответствии с рекомендациями Microsoft по разработке коллекций в случае, если в метод передано значение индекса несуществующего элемента, метод Item должен возвращать значение null.
Исходный код коллекции строк аналогичен коду типизированной коллекции:
public interface IStringTestCollection { //... [DispId(0)] string Item([In] int index); //... } publicclass StringTestCollection : IStringTestCollection { //...publicstring Item(int index) { try { return items[index - 1]; } catch { returnnull; } } //...string[] items; } |
Отметим, что в методе Item возможен возврат null, т.к. строки допускают значение null.
Иная ситуация с коллекциями, элементы которых имеют value-тип, например, целые числа.
Под термином «value-type коллекции» здесь понимаются коллекции, элементы которых имеют простой тип, совместимый с COM, – int, double, bool, DateTime и др. (о соответствии .Net-типов COM-типам в документации .Net Framework SDK написано достаточно подробно). К этой же категории можно отнести и коллекции, элементы которых являются структурами (структуры должны быть совместимы с COM).
Отличие value-type коллекций от коллекций объектов и строк заключается в том, что метод Item не может возвращать значение null. В случае же передачи в качестве параметра метода некорректного индекса можно сгенерировать соответствующее COM-исключение. Подобную технику, кстати, можно использовать и для коллекций объектов и строк.
Рассмотрим в качестве примера реализацию целочисленной коллекции:
public interface IIntTestCollection { //... [DispId(0)] int Item([In] int index); //... } publicclass IntTestCollection : IIntTestCollection { //...publicint Item(int index) { return items[index - 1]; } //...int[] items; } |
Отметим, что в методе Item в случае некорректного индекса сгенерируется .Net-исключение IndexOutOfRangeException, которое автоматически будет преобразовано в соответствующую COM-ошибку (80131508 – Index was outside the bounds of the array).
До сих пор рассматривались коллекции с целочисленной индексацией элементов. В общем же случае индексом коллекции может выступать произвольное значение. Так, например, коллекция Documents COM-объекта Word.Application обеспечивает доступ к своим элементам двумя способами:
Documents(1).Activate
Documents("Report.doc").Activate
|
Т.е. в качестве индексов элементов могут выступать как целые числа, так и строковые значения.
Построим подобную коллекцию:
public interface IVariantIndexTestCollection { //... [DispId(0)] int Item([In] object index); //... } publicclass VariantIndexTestCollection : IVariantIndexTestCollection { //...publicint Item(object index) { string s = index asstring; if (s != null) { if (s == "один") return items[0]; elseif (s == "два") return items[1]; elsethrownew IndexOutOfRangeException(); } else { try { return items[Convert.ToInt32(index) - 1]; } catch (IndexOutOfRangeException e) { throw e; } catch { thrownew ArgumentException(); } } } //...int[] items; } |
Здесь необходимо обратить внимание на то, что параметр index метода Item имеет тип object, который соответствует типу variant в COM. И, конечно же, из-за необходимости проверки реального типа параметра реализация метода несколько усложняется.
И еще один момент – использование класса Convert позволяет унифицировать обработку разных целочисленных типов (byte, sbyte, short, ushort, int, uint).
Рассмотренные в настоящей статье коллекции, естественно, максимально упрощены. Тем не менее, можно надеяться, что описанные техники и приведенный в качестве примеров код помогут сделать разработку реальных COM-коллекций достаточно простой.
Полные исходные тексты прилагаются.
Исходные тексты C# реализованы как дополнение к проекту <SDK>\Samples\Technologies\Interop\Applications\LoanApps\COMtoNET\loanlib. Для сборки и регистрации тестовой библиотеки выполните следующее:
Исходные тексты примеров использования коллекций находятся в файле Samples.bas. Тестирование выполнялось в Excel 2003 (файл Test.xls), и файл Samples.bas был экспортирован из VBA. Поэтому исходный код процедур необходимо скопировать в ту среду исполнения, в которой будут выполняться прилагаемые примеры. Кроме того, если в среде исполнения требуется указывать ссылки на внешние библиотеки COM-объектов, необходимо подключить библиотеку LoanLib.dll.
В завершение хотелось бы заметить, что в статье довольно мало исходного кода. И это, наверное, хорошо, когда такие сложные задачи, как создание коллекций, решаются так просто. И все это возможно благодаря тому, что .Net обеспечивает очень мощные и в то же время простые в использовании технологии для работы с COM.
Сообщений 4 Оценка 15 [+0/-1] Оценить |