Оценка 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.
Оценка 15
[+0/-1]
Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|