![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
Введение Немного истории Специфика .NET Технология создания внешних компонентов 1С Конфигурирование COM+ Использование Заключение Литература | ![]() |
Данная статья описывает технологию создания COM+-компонента для 1С на C#. Код компонента был написан с использованием Microsoft Visual Studio.NET 2003, классов Microsoft .NET Framework 1.1 и протестирован на операционной системе Windows Server 2003 System.
Столкнувшись с задачей написания внешнего компонента для интеграции сканера штрих-кодов с 1C, я решил сделать это на C#, так как последнее время в основном работал с этим языком. Попытался найти какой-нибудь пример в Internet, но кроме проектов на MFC, ATL и Delphi ничего не обнаружил. Зато помогли статьи описывающие технологию создания внешних компонентов, особенно потому, что опыта работы с 1С у меня практически не было.
Пространство имен System.EnterpriseServices предоставляет ряд классов и атрибутов для реализации .NET-компонентов, работающих с сервисами COM+. Базовым классом для этих компонентов является ServicedComponent, благодаря которому написание СOM+-приложения сводится к следующим тривиальным шагам [1]:
1. Декларации интерфейсов, реализуемых компонентом, и создание класса, производного от ServicedComponent:
using System; using System.EnterpriseServices; public interface IInitDone {} public interface ILanguageExtender {} public class Component : ServicedComponent, IInitDone, ILanguageExtender { } |
2. Конфигурированию приложения, компонента и его методов с использованием атрибутов. Код, представленный ниже, показывает применение атрибутов:
using System.Reflection; using System.Runtime.InteropServices; . . . [assembly: ApplicationAccessControl(AccessChecksLevel=AccessChecksLevelOption.Application)] [assembly: ApplicationName("SimpleExternalComponent Application")] [assembly: ApplicationActivation(ActivationOption.Library)] [assembly: AssemblyKeyFile("filename.snk")] . . . [ClassInterface(ClassInterfaceType.None)] [ProgId("AddIn.SimpleExternalComponent")] [Description("SimpleExternalComponent.Component class")] public class Component : ServicedComponent . . . . . . |
СОВЕТ Атрибуты ClassInterface и ProgId определены в System.Runtime.InteropServices, а AssemblyKeyFile - в System.Reflection. |
3. Компиляции кода. Я предпочитаю делать это в Visual Studio .NET, но можно компилировать и из командной строки.
csc /t:library SimpleExternalComponent.cs |
4. Регистрации и инсталляции компонента, которая может быть осуществлена несколькими способами.
ПРИМЕЧАНИЕ В MSDN прямо говорится, что для того, чтобы использоваться в COM+, сборка должна быть подписана. Но на самом деле это необязательно, достаточно в свойствах проекта указать, что требуется регистрация сборки как COM-объекта. |
Задача реализации и импорта unmanaged COM-компонентов в .NET-приложение связана с использованием классов из пространства имен System.Runtime.InteropServices. Функции интерфейсов должны быть декларированы таким образом, чтобы параметры соответствовали unmanaged-декларации в IDL-файле. Для этого используется атрибут MarshalAs, применение которого описано в MSDN [3,4]. Ниже приведен пример одной из функций интерфейса IInitDone в unmanaged-коде:
HRESULT GetInfo(SAFEARRAY * * pInfo); |
В .NET эта функция декларируется следующим образом:
void GetInfo([MarshalAs(UnmanagedType.SafeArray,
SafeArraySubType=VarEnum.VT_VARIANT)]
ref object[] info);
|
Данное пространство имен содержит классы и интерфейсы для получения доступа к загруженным типам .NET в период выполнения. 1С обращается к внешнему компоненту посредством интерфейса ILanguageExtender, методы которого реализуют логику нахождения вызываемых методов по имени. Это позволяет кэшировать информацию в период инициализации компонента.
Представленный ниже код показывает использование классов System.Reflection для реализации метода RegisterExtentionAs интерфейса ILanguageExtender. Данный метод кэширует информацию о методах и свойствах пользовательских интерфейсов.
/// <summary> /// Register component in 1C /// </summary> /// <param name="extensionName"></param> public void RegisterExtensionAs( [MarshalAs(UnmanagedType.BStr)] ref String extensionName) { try { // initialize data members nameToNumber = new Hashtable(); numberToName = new Hashtable(); numberToParams = new Hashtable(); numberToRetVal = new Hashtable(); propertyNameToNumber = new Hashtable(); propertyNumberToName = new Hashtable(); numberToMethodInfoIdx = new Hashtable(); propertyNumberToPropertyInfoIdx = new Hashtable(); // Заполнение хэш-таблиц Type type = this.GetType(); Type[] allInterfaceTypes = type.GetInterfaces(); // Определение идентификатора int Identifier = 0; foreach(Type interfaceType in allInterfaceTypes) { if ( !interfaceType.Name.Equals("IDisposable") && !interfaceType.Name.Equals("IManagedObject") && !interfaceType.Name.Equals("IRemoteDispatch") && !interfaceType.Name.Equals("IServicedComponentInfo") && !interfaceType.Name.Equals("IInitDone") && !interfaceType.Name.Equals("ILanguageExtender") ) { // Обработка методов MethodInfo[] interfaceMethods = interfaceType.GetMethods(); foreach (MethodInfo interfaceMethodInfo in interfaceMethods) { nameToNumber.Add(interfaceMethodInfo.Name, Identifier); numberToParams.Add(Identifier, interfaceMethodInfo.GetParameters().Length); if (typeof(void) != interfaceMethodInfo.ReturnType) numberToRetVal.Add(Identifier, true); Identifier++; } // Обработка свойств PropertyInfo[] interfaceProperties = interfaceType.GetProperties(); foreach (PropertyInfo interfacePropertyInfo in interfaceProperties) { propertyNameToNumber.Add(interfacePropertyInfo.Name, Identifier); Identifier++; } } } foreach (DictionaryEntry entry in nameToNumber) numberToName.Add(entry.Value, entry.Key); foreach (DictionaryEntry entry in propertyNameToNumber) propertyNumberToName.Add(entry.Value, entry.Key); // Сохранение информации о методах класса allMethodInfo = type.GetMethods(); // Сохранение информации о свойствах класса allPropertyInfo = type.GetProperties(); // Отображение номера метода на индекс в массиве foreach (DictionaryEntry entry in numberToName) { bool found = false; for(int ii = 0; ii < allMethodInfo.Length; ii++) { if (allMethodInfo[ii].Name.Equals(entry.Value.ToString())) { numberToMethodInfoIdx.Add(entry.Key, ii); found = true; } } if (false == found) throw new COMException("Метод не реализован "); } // Отображение номера свойства на индекс в массиве foreach (DictionaryEntry entry in propertyNumberToName) { bool found = false; for(int ii = 0; ii < allPropertyInfo.Length; ii++) { if (allPropertyInfo[ii].Name.Equals(entry.Value.ToString())) { propertyNumberToPropertyInfoIdx.Add(entry.Key, ii); found = true; } } if (false == found) throw new COMException("The property is not implemented"); } // Компонент инициализирован успешно // Возвращаем имя компонента extensionName = componentName; } catch (Exception) { return; } } |
ПРИМЕЧАНИЕ Интерфейсы IDisposable, IManagedObject, IRemoteDispatch и IServicedComponentInfo реализованы в классе ServicedComponent. |
Все описываемые 1С-интерфейсы имеют предопределенные GUID. Глобальный идентификатор может быть присвоен приложениям, классам, интерфейсам, структурам и другим конструкциям .NET с помощью атрибута GuidAttribute. В противном случае GUID генерируется автоматически. Рекомендуется использовать GuidAttribute только в случаях, где это необходимо, так как его использование затрудняет контроль версий.
Внешний компонент 1С должен реализовывать, по крайней мере, два интерфейса – IInitDone и ILanguageExtender. Первый служит для инициализации компонента, а второй - для вызова его функций, процедур и свойств из среды 1С [5]. Другие интерфейсы, описанные в этой статье, реализованы 1С и используются внешним компонентом для генерации событий и получения доступа к строке состояния.
Функции данного интерфейса вызываются при подключении компонента. Представленный ниже код декларирует интерфейс IInitDone в .NET приложении:
[Guid("AB634001-F13D-11d0-A459-004095E1DAEA")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IInitDone { /// <summary> /// Инициализация компонента /// </summary> /// <param name="connection">reference to IDispatch</param> void Init( [MarshalAs(UnmanagedType.IDispatch)] object connection); /// <summary> /// Вызывается перед уничтожением компонента /// </summary> void Done(); /// <summary> /// Возвращается инициализационная информация /// </summary> /// <param name="info">Component information</param> void GetInfo( [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VarEnum.VT_VARIANT)] ref object[] info); } |
Ниже показано, как функции Init и GetInfo реализованы в классе компонента:
private IAsyncEvent asyncEvent = null; private IStatusLine statusLine = null; /// <summary> /// Инициализация компонента /// </summary> /// <param name="connection">reference to IDispatch</param> public void Init( [MarshalAs(UnmanagedType.IDispatch)] object connection) { asyncEvent = (IAsyncEvent)connection; statusLine = (IStatusLine)connection; } /// <summary> /// Возвращается информация о компоненте /// </summary> /// <param name="info">Component information</param> public void GetInfo( [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VarEnum.VT_VARIANT)] ref object[] info) { info[0] = 2000; } |
Данный интерфейс определяет логику вызова функций, процедур и свойств внешнего компонента из 1С.
ПРИМЕЧАНИЕ Аналогом ILanguageExtender является интерфейс IDispatch, используемый в OLE Automation. В действительности, 1С может выступать в качестве клиента OLE Automation. В этом случае вызов функций сервера автоматизации осуществляется с помощью функции Invoke интерфейса IDispatch, что замедляет процесс взаимодействия в связи с преобразованием передаваемых параметров из типа Variant и обратно [6]. Поэтому рекомендуется использовать пользовательские интерфейсы. |
/// <summary> /// При вызове функции из 1С производится следующая последовательность вызовов: /// 1. FindMethod /// 2. GetNParams /// 3. HasRetVal /// </summary> [Guid("AB634003-F13D-11d0-A459-004095E1DAEA")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface ILanguageExtender { /// <summary> /// Регистрация компонента в 1C /// </summary> /// <param name="extensionName"></param> /// <remarks> /// <prototype> /// [helpstring("method RegisterExtensionAs")] /// HRESULT RegisterExtensionAs([in,out]BSTR *bstrExtensionName); /// </prototype> /// </remarks> void RegisterExtensionAs( [MarshalAs(UnmanagedType.BStr)] ref String extensionName); /// <summary> /// Возвращается количество свойств /// </summary> /// <param name="props">Количество свойств </param> /// <remarks> /// <prototype> /// HRESULT GetNProps([in,out]long *plProps); /// </prototype> /// </remarks> void GetNProps(ref Int32 props); /// <summary> /// Возвращает целочисленный идентификатор свойства, соответствующий /// переданному имени /// </summary> /// <param name="propName">Имя свойства</param> /// <param name="propNum">Идентификатор свойства</param> /// <remarks> /// <prototype> /// HRESULT FindProp([in]BSTR bstrPropName,[in,out]long *plPropNum); /// </prototype> /// </remarks> void FindProp( [MarshalAs(UnmanagedType.BStr)] String propName, ref Int32 propNum); /// <summary> /// Возвращает имя свойства, соответствующее /// переданному целочисленному идентификатору /// </summary> /// <param name="propNum">Идентификатор свойства</param> /// <param name="propAlias"></param> /// <param name="propName">Имя свойства</param> /// <remarks> /// <prototype> /// HRESULT GetPropName([in]long lPropNum,[in]long lPropAlias,[in,out]BSTR *pbstrPropName); /// </prototype> /// </remarks> void GetPropName( Int32 propNum, Int32 propAlias, [MarshalAs(UnmanagedType.BStr)] ref String propName); /// <summary> /// Возвращает значение свойства. /// </summary> /// <param name="propNum">Идентификатор свойства </param> /// <param name="StringpropVal">Значение свойства</param> /// <remarks> /// <prototype> /// </prototype> /// </remarks> void GetPropVal( Int32 propNum, [MarshalAs(UnmanagedType.Struct)] ref object propVal); /// <summary> /// Устанавливает значение свойства. /// </summary> /// <param name="propName">Имя свойства</param> /// <param name="propVal">Значение свойства</param> /// <remarks> /// <prototype> /// HRESULT SetPropVal([in]long lPropNum,[in]VARIANT *varPropVal); /// </prototype> /// </remarks> void SetPropVal( Int32 propNum, [MarshalAs(UnmanagedType.Struct)] ref object propVal); /// <summary> /// Определяет, можно ли читать значение свойства /// </summary> /// <param name="propNum"> Идентификатор свойства </param> /// <param name="propRead">Флаг читаемости</param> /// <remarks> /// <prototype> /// HRESULT IsPropReadable([in]long lPropNum,[in,out]BOOL *pboolPropRead); /// </prototype> /// </remarks> void IsPropReadable(Int32 propNum, ref bool propRead); /// <summary> /// Определяет, можно ли изменять значение свойства /// </summary> /// <param name="propNum">Идентификатор свойства</param> /// <param name="propRead">Флаг записи</param> /// <remarks> /// <prototype> /// HRESULT IsPropWritable([in]long lPropNum,[in,out]BOOL *pboolPropWrite); /// </prototype> /// </remarks> void IsPropWritable(Int32 propNum, ref Boolean propWrite); /// <summary> /// Возвращает количество методов /// </summary> /// <param name="pMethods">Количество методов</param> /// <remarks> /// <prototype> /// [helpstring("method GetNMethods")] /// HRESULT GetNMethods([in,out]long *plMethods); /// </prototype> /// </remarks> void GetNMethods(ref Int32 pMethods); /// <summary> /// Возвращает идентификатор метода по его имени /// </summary> /// <param name="methodName">Имя метода</param> /// <param name="methodNum">Идентификатор метода</param> /// <remarks> /// <prototype> /// HRESULT FindMethod(BSTR bstrMethodName,[in,out]long *plMethodNum); /// </prototype> /// </remarks> void FindMethod( [MarshalAs(UnmanagedType.BStr)] String methodName, ref Int32 methodNum); /// <summary> /// Возвращает имя метода по его идентификатору /// </summary> /// <param name="methodNum">Идентификатор метода</param> /// <param name="methodAlias"></param> /// <param name="methodName">Имя метода</param> /// <remarks> /// <prototype> /// HRESULT GetMethodName([in]long lMethodNum,[in]long lMethodAlias,[in,out]BSTR *pbstrMethodName); /// </prototype> /// </remarks> void GetMethodName(Int32 methodNum, Int32 methodAlias, [MarshalAs(UnmanagedType.BStr)] ref String methodName); /// <summary> /// Возвращает количество параметров метода по его идентификатору /// </summary> /// <param name="methodNum">Идентификатор метода</param> /// <param name="pParams">Количество параметров</param> /// <remarks> /// <prototype> /// HRESULT GetNParams([in]long lMethodNum, [in,out]long *plParams); /// </prototype> /// </remarks> void GetNParams(Int32 methodNum, ref Int32 pParams); void GetParamDefValue( Int32 methodNum, Int32 paramNum, [MarshalAs(UnmanagedType.Struct)] ref object paramDefValue); /// <summary> /// Указывает, что у метода есть возвращаемое значение /// </summary> /// <param name="methodNum">Идентификатор метода</param> /// <param name="retValue">Наличие возвращаемого значения</param> /// <remarks> /// <prototype> /// HRESULT HasRetVal([in]long lMethodNum,[in,out]BOOL *pboolRetValue); /// </prototype> /// </remarks> void HasRetVal(Int32 methodNum, ref Boolean retValue); /// <summary> /// Вызов метода как процедуры с использованием идентификатора /// </summary> /// <param name="methodNum">Идентификатор метода</param> /// <param name="pParams">Параметры</param> /// <remarks> /// <prototype> /// HRESULT CallAsProc([in]long lMethodNum,[in] SAFEARRAY (VARIANT) *paParams); /// </prototype> /// </remarks> void CallAsProc( Int32 methodNum, [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VarEnum.VT_VARIANT)] ref object[] pParams); /// <summary> /// Вызов метода как функции с использованием идентификатора /// </summary> /// <param name="methodNum">Идентификатор метода</param> /// <param name="retValue">Возвращаемое значение</param> /// <param name="pParams">Параметры</param> /// <remarks> /// <prototype> /// HRESULT CallAsFunc([in]long lMethodNum,[in,out] VARIANT *pvarRetValue, /// [in] SAFEARRAY (VARIANT) *paParams); /// </prototype> /// </remarks> void CallAsFunc( Int32 methodNum, [MarshalAs(UnmanagedType.Struct)] ref object retValue, [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VarEnum.VT_VARIANT)] ref object[] pParams); } |
1С вызывает функции внешнего компонента в следующей последовательности: FindMethod, GetNParams, HasRetVal, CallAsFunc. Ниже приведена реализация этих методов в классе компонента:
/// <summary> /// Возвращает идентификатор метода /// </summary> /// <param name="methodName">Имя метода</param> /// <param name="methodNum">Идентификатор метода</param> /// <remarks> /// <prototype> /// [helpstring("method FindMethod")] /// HRESULT FindMethod(BSTR bstrMethodName,[in,out]long *plMethodNum); /// </prototype> /// </remarks> public void FindMethod( [MarshalAs(UnmanagedType.BStr)] String methodName, ref Int32 methodNum) { methodNum = (Int32)nameToNumber[methodName]; } /// <summary> /// Возвращает число параметров метода по его идентификатору /// </summary> /// <param name="methodNum">Идентификатор метода</param> /// <param name="pParams">Число параметров</param> /// <remarks> /// <prototype> /// HRESULT GetNParams([in]long lMethodNum,[in,out]long *plParams); /// </prototype> /// </remarks> public void GetNParams(Int32 methodNum, ref Int32 pParams) { pParams = (Int32)numberToParams[methodNum]; } /// <summary> /// Указывает, что у метода есть возвращаемое значение /// </summary> /// <param name="methodNum">Идентификатор метода</param> /// <param name="retValue">Наличие возвращаемого значения</param> /// <remarks> /// <prototype> /// HRESULT HasRetVal([in]long lMethodNum,[in,out]BOOL *pboolRetValue); /// </prototype> /// </remarks> public void HasRetVal(Int32 methodNum, ref Boolean retValue) { retValue = (Boolean)numberToRetVal[methodNum]; } /// <summary> /// Вызов метода как функции с использованием идентификатора /// </summary> /// <param name="methodNum">Идентификатор метода</param> /// <param name="retValue">Возвращаемое значение</param> /// <param name="pParams">Параметры</param> /// <remarks> /// <prototype> /// HRESULT CallAsFunc( [in]long lMethodNum,[in,out] VARIANT *pvarRetValue, /// [in] SAFEARRAY (VARIANT)*paParams); /// </prototype> /// </remarks> public void CallAsFunc( Int32 methodNum, [MarshalAs(UnmanagedType.Struct)] ref object retValue, [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VarEnum.VT_VARIANT)] ref object[] pParams) { retValue = allMethodInfo[(int)numberToMethodInfoIdx[methodNum]].Invoke( this, pParams); } |
Данный интерфейс реализован 1С для получения событий от внешнего компонента. Ссылка на него получена путем приведения параметра функции Init интерфейса IInitDone к типу IAsyncEvent при инициализации компонента. Внешнее событие обрабатывается 1С в функции ОбработкаВнешнегоСобытия.
[Guid("ab634004-f13d-11d0-a459-004095e1daea")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IAsyncEvent // : IUnknown { /// <summary> /// Установка глубины буфера событий /// </summary> /// <param name="depth">Buffer depth</param> /// <remarks> /// <prototype> /// HRESULT SetEventBufferDepth(long lDepth); /// </prototype> /// </remarks> void SetEventBufferDepth(Int32 depth); /// <summary> /// Получение глубины буфера событий /// </summary> /// <param name="depth">Buffer depth</param> /// <remarks> /// <prototype> /// HRESULT GetEventBufferDepth(long *plDepth); /// </prototype> /// </remarks> void GetEventBufferDepth(ref long depth); /// <summary> /// Посылка события /// </summary> /// <param name="source">Event source</param> /// <param name="message">Event message</param> /// <param name="data">Event data</param> /// <remarks> /// <prototype> /// HRESULT GetEventBufferDepth(long *plDepth); /// </prototype> /// </remarks> void ExternalEvent( [MarshalAs(UnmanagedType.BStr)] String source, [MarshalAs(UnmanagedType.BStr)] String message, [MarshalAs(UnmanagedType.BStr)] String data ); /// <summary> /// Очистка буфера событий /// </summary> /// <remarks> /// <prototype> /// </prototype> /// </remarks> void CleanBuffer(); } |
С помощью этого интерфейса внешний компонент получает доступ к строке состояния 1С. Ссылка на интерфейс получена методом, описанным в IAsyncEvent.
[Guid("AB634005-F13D-11D0-A459-004095E1DAEA")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IStatusLine { /// <summary> /// Задает текст статусной строки /// </summary> /// <param name="bstrStatusLine">Текст статусной строки</param> /// <remarks> /// <prototype> /// HRESULT SetStatusLine(BSTR bstrStatusLine); /// </prototype> /// </remarks> void SetStarusLine( [MarshalAs(UnmanagedType.BStr)]String bstrStatusLine ); /// <summary> /// Сброс статусной строки /// </summary> /// <remarks> /// <propotype> /// HRESULT ResetStatusLine(); /// </propotype> /// </remarks> void ResetStatusLine(); } |
Подключить внешний компонент к 1С можно двумя методами:
Если ПодключитьВнешнююКомпоненту("AddIn.SimpleExternalComponent") = 0 Тогда Предупреждение("Не удалось загрузить компонент" + симв(13) + симв(10) + "AddIn.SimpleExternalComponent!", 60); ЗавершитьРаботуСистемы(0); Возврат; КонецЕсли; Компонента = СоздатьОбъект("AddIn.SimpleExternalComponent"); |
Конфигурирование COM+-приложения осуществляется с помощью атрибутов пространства имен System.EnterpriseServices. Например, приведенный ниже атрибут устанавливает активацию в процессе вызывающего приложения.
[assembly: ApplicationActivation(ActivationOption.Library)] |
Аналогичного эффекта можно добиться, используя Component Services (COM+) management tool
Для обеспечения безопасности можно осуществлять контроль доступа на уровне интерфейсов и методов. Данная опция также задается двумя альтернативными способами: с помощью атрибута ApplicationAccessControl или через Component Services (COM+) management tool.
Представленный исходный код может быть использован для написания пользовательских компонентов. Для добавления новых методов разработчик должен выполнить следующие действия:
У COM+-компонентов есть ряд достоинств, которые я не буду детально описывать в этой статье. Достаточно упомянуть поддержку транзакций, Web-сервисов, Just in time активацию, ролевую безопасность и т. д. [9].
Благодаря использованию классов пространства System.Reflection время разработки 1C-компонента существенно сокращается. Разработчик может сосредоточиться на реализации пользовательских методов, не вникая в логику интерфейсов 1С.
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |