and it will, I hope, soon seem
as clear as a mountain creek!
Bertrand Meyer
Любой программист, которому приходилось написать несколько внешних компонент 1С:Предприятия, наверняка задумывался о странностях реализации интерфейсов IInitDone и ILanguageExtender. Зачем каждый раз полностью копировать реализацию некоторых методов, вроде FindProp или GetPropName? Почему так неудобно передаются параметры вызовов из 1С в CallAsProp и CallAsFunc в виде массивов? И почему нарушен один из основополагающих принципов объектно-ориентированного программирования (ООП) — прямое отображение? (Речь идет о таинственном объекте в 1С, который создается с помощью
ОбъектКомпоненты = Новый("AddIn.SomeName");
но который никак не представлен во внешней компоненте!). В этой статье автором предложен набор классов, который скрывает сложности служебных интерфейсов 1С, включая все трудности маршаллига COM-интерфейсов параметров 1С:Предприятия и позволяет написать внешнюю компоненту, просто создав обычный класс .Net на C#.
КАК ПОЛЬЗОВАТЬСЯ
Сначала покажем, как написать внешнюю компоненту с помощью библиотеки, прилагаемой в качестве примера к данной статье. Читатели не интересующиеся архитектурой, следующий раздел могут не читать. Но оценить, насколько все стало проще и элегантнее, наверное, сможет каждый. Настройки проекта в Visual Studio, как зарегистрировать dll внешней компоненты и другие нюансы, существенные для начинающих, здесь не объясняются.
Итак, вначале создадим в проекте на C# в Visual Studio типа «Class library» новый открытый (public) класс. Он может быть статическим, тогда наличие (разумеется статического) конструктора не обязательно, либо обычным динамическим, в этом случае обязательное требование — присутствие публичного конструктора по-умолчанию (без параметров), так как это класс и будет представлением того самого виртуального объекта 1С AddIn. Дальше также просто: открытые свойства класса — это свойства объекта, если свойство только get — значит в 1С оно будет доступно только для чтения, set — для записи. Открытые методы класса — методы объекта AddIn с полным соответствием с возвращаемым значением и параметрами. Например:
public class SomeName
{
public SomeName()
{
InstanceName = "SomeName";
}
public bool IsEnabled {get; set;}
public string InstanceName {get; private set;}
public object Make(string How, int Count)
{
if (IsEnabled)
{
InstanceName = String.Empty;
for (int i=0;i<Count;++i)
V8Context.CreateContext().V8Message(MessageTypes.Info,How);
}
return null;
}
}
такой класс будет виден в 1С как объект AddIn.SomeName со свойствами IsEnabled типа булево для чтения и записи, InstanceName — только для чтения и методом Make, возвращающим произвольное значение. Не забываем об ограничениях типов 1С:Предприятия: параметры и свойства могут быть типов string, bool, int, double, decimal, DateTime и object, который может инкапсулировать перечисленные до него типы, содержать null (в 1С Неопределено) либо передавать COM-объект, созданный в компоненте и реализующий интерфейс IDispatch. (В net это реализуется созданием класса с атрибутами [ComVisible] и [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]). Возможна передача массивов этих типов, но в 1С они будут соответствовать не объекту Массив, а COMSafeArray. Класс конечно же может иметь и скрытые (private или protected) поля, свойства, методы, они будут скрыты и для 1С.
После создания и реализации класса, необходимо «подключить» его к библитеке. Для этого копируем папку AddIn из шаблона библиотеки и создаем простой класс-заглушку:
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using V8.AddIn;
[ComVisible(true)]
[Guid("6a81d0a9-6441-463f-a0c9-ec7b1f2cbd56")] // произвольный Guid-идентификатор Вашей компоненты
[ProgId("AddIn.SomeComponent")] // это имя COM-объекта, по которому Вы будете ее подключать
public class Some : LanguageExtenderAddIn
{
public Some() : base(typeof(SomeName), 1000) {}
}
Класс LanguageExtenderAddIn содержит реализацию всей интерфейсной части компоненты, в его конструктор передается описание типа объекта компоненты и номер версии (он может быть 1000 или 2000). Подключается компонента инструкцией:
ПодключитьВнешнююКомпоненту("AddIn.SomeComponent");
В целом все, проект собирается и компонента готова. Остались мелкие детали.
Вывод сообщений, дополнительные интерфейсы. Реализованы с помощью класса V8Context. Это синглетон, его не следует создавать явно, а вызывать конструкцией:
V8Context.CreateV8Context()
Он содержит перезагруженные методы V8Message, выводящие текст в окно сообщений или диалог с предупреждением об ошибке. Например,
V8Context.CreateV8Context().V8Message(MessageTypes.MsgboxInfo, "Текст сообщения");
выведет информационный диалог с текстом. Также класс V8Context имеет свойство AsyncEvent, реализующее интерфейс IAsyncEvent для отправки сообщений в 1С.
Русскоязычные синонимы, параметры по умолчанию устанавливаются с помощью атрибутов Alias и HasDefaultValue. В качестве примера, дополним класс SomeName:
public class SomeName
{
public SomeName() {}
[Alias("Включена")]
public bool IsEnabled {get; set;}
[Alias("ИмяЭкземпляра")]
public string InstanceName {get; private set;}
[Alias("Выполнить")]
public object Make(string How, [HasDefaultValue(1)] int Count) { return null;}
}
Второй параметр метода Make при вызове из 1С:Предприятия может быть пропущен, в этом случае он имеет значение 1. Покажем примерный код в 1С для работы с этой компонентой:
ПодключитьВнешнююКомпоненту("AddIn.SomeComponent");
ОбъектКомпоненты = Новый("AddIn.SomeName");
ОбъектКомпоненты.Включена = Истина;
Если ПустаяСтрока(ОбъектКомпоненты.InstanceName) Тогда
ОбъектКомпоненты.Выполнить("Быстро");
КонецЕсли;
ИНТЕРФЕЙСЫ ВНУТРЬ И НАРУЖУ
(УСТРОЙСТВО БИБЛИОТЕКИ)
Для улучшения какого-либо программного решения, необходимо выяснить, что в нем не устраивает. Изучив примеры создания внешних компонент с дисков ИТС, ответ напрашивается сам собой — отсутствие подходящего коннектора между addin объектом компоненты и 1С:Предприятием. И дело не в том, что в примерах он (как класс) вообще не реализован, а в том что даже написав его, мы были бы вынуждены вызывать свойства и методы через их описания массивами строк. Код этого объекта все равно бы находился внутри класса, реализующего ILanguageExtender. Net Framework с самой первой версии имеет замечательный механизм отражения своих метаданных — пространство имен Reflection. Полезность его использования при создании компонент была отмечена в статье (http://rsdn.ru/article/dotnet/cs1c.xml), но автор остановился на пол пути и не отделил реализацию компоненты от интерфейсной части 1С.
Технология внешних компонент предоставляет ряд интерфейсов, из которых IInitDone и ILanguageExtender нужны только для использования внутри 1С. 1С:Предприятие по ним распознает внешнюю компоненту и управляет ей. Для программиста их реализация — лишняя работа, поэтому мы попытаемся их отделить. Начнем с IInitDone, имеющего три метода и создадим его реализацию:
using System;
using System.Runtime.InteropServices;
namespace V8.AddIn
{
[ComVisible(true), Guid("bc631c98-2f0b-49b9-b722-b7e223e46059")]
public abstract class InitAddIn : IInitDone
{
private int m_Version;
protected InitAddIn(int Version)
{
this.m_Version = Version;
}
void IInitDone.Init([MarshalAs(UnmanagedType.IDispatch)] object pConnection)
{
new V8Context(pConnection);
}
void IInitDone.Done()
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
void IInitDone.GetInfo([MarshalAs(UnmanagedType.SafeArray)] ref object[] pInfo)
{
pInfo.SetValue(this.m_Version, 0);
}
}
}
подключение компоненты начинается с вызова Init, в нем мы инициализируем V8Context объектом 1С. Оставшийся код очевиден: храним версию компоненты и принудительно собираем мусор при закрытии 1С в методе Done. В принципе после этого уже можно написать компоненту, например так:
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using V8.AddIn;
[ComVisible(true)]
[Guid("73D8A32F-8195-4482-B845-71B6535DC079")]
public class VoidComponent : InitAddIn
{
public VoidComponent() : base(1000) {}
}
Это компонента, не реализующая расширения встроенного языка, о ней многие забывают. Такие компоненты могут вызывать внешние события в 1С, например от какого-то оборудования и выводить сообщения. При реализации ILanguageExtender прийдется воспользоваться всей мощью пространства Reflection. Как уже было видно в примере, в конструктор абстрактного класса LanguageExtenderAddIn передается описание типа класса компоненты (Type). Через него необходимо выполнять вызовы свойств и методов класса. Вначале посмотрим на объвление, конструктор и служебные члены класса:
using System;
using System.Reflection;
using System.Runtime.InteropServices;
namespace V8.AddIn
{
[Guid("43295454-83da-49a0-beca-58a9f6ac1ef0"), ComVisible(true)]
public abstract class LanguageExtenderAddIn : InitAddIn, ILanguageExtender
{
private string m_Name;
private PropertyInfo[] m_Properties;
private MethodInfo[] m_Methods;
private object m_Wrapper;
private void InitWrapperInfo(Type WrapperType, BindingFlags flags)
{
this.m_Name = WrapperType.Name;
this.m_Properties = WrapperType.GetProperties(flags);
this.m_Methods = WrapperType.GetMethods(flags);
}
protected LanguageExtenderAddIn(Type WrapperType, int Version) : base(Version)
{
ConstructorInfo constructor = WrapperType.GetConstructor(Type.EmptyTypes);
if (constructor == null)
{
this.InitWrapperInfo(WrapperType, BindingFlags.Static | BindingFlags.Public);
return;
}
this.m_Wrapper = constructor.Invoke(null);
this.InitWrapperInfo(WrapperType, BindingFlags.Instance | BindingFlags.Public);
}
Объект m_Wrapper — это экземпляр класса компоненты. Мы храним описание его свойств и методов в массивах m_Properties и m_Methods. При отсутствии конструктора по умолчанию, полагаем что имеем дело со статическим классом, для которого вызов Invoke не нужен. Покажем два простых примера, реализацию RegisterExtensionAs и GetNProps:
void ILanguageExtender.RegisterExtensionAs([MarshalAs(UnmanagedType.BStr)] ref string bstrExtensionName)
{
bstrExtensionName = m_Name;
}
void ILanguageExtender.GetNProps(ref int plProps)
{
plProps = this.m_Properties.GetLength(0);
}
Далее при реализации методов ILanguageExtender необходмо вызывать обращаться к классу компоненты через их описания, не забывая существование русских синонимов (для этого нужно получать значения атрибута Alias):
void ILanguageExtender.FindProp([MarshalAs(UnmanagedType.BStr)] string bstrPropName, ref int plPropNum)
{
plPropNum = 0;
Type typeFromHandle = typeof(AliasAttribute);
for (int i = 0; i <= this.m_Properties.GetUpperBound(0); i++)
{
AliasAttribute aliasAttribute = (AliasAttribute)Attribute.GetCustomAttribute(this.m_Properties[i], typeFromHandle);
if (this.m_Properties[i].Name.ToUpper() == bstrPropName.ToUpper() || (aliasAttribute != null && aliasAttribute.AliasName.ToUpper() == bstrPropName.ToUpper()))
{
plPropNum = ++i;
return;
}
}
}
void ILanguageExtender.GetPropVal(int lPropNum, ref object pvarPropVal)
{
PropertyInfo propertyInfo = this.m_Properties[lPropNum - 1];
try
{
pvarPropVal = propertyInfo.GetValue(this.m_Wrapper, null);
}
catch (Exception ex)
{
V8Context v8Context = V8Context.CreateV8Context();
if (ex.InnerException!=null)
v8Context.V8Message(MessageTypes.Fail, ex.InnerException.Message, ex.InnerException.Source);
else
v8Context.V8Message(MessageTypes.Fail, ex.Message, ex.Source);
}
}
Обработку исключений необходимо делать в компоненте, так как любой Exception из net framework в 1С будет восприниматься как исключение в mscorlib.dll без расшифровки. В следующем примере показана реализация GetParamDefValue и обработка атрибута HasDefaultValue:
void ILanguageExtender.GetParamDefValue(int lMethodNum, int lParamNum, ref object pvarParamDefValue)
{
ParameterInfo element = this.m_Methods[lMethodNum - 1].GetParameters()[lParamNum];
HasDefaultValueAttribute hasDefaultValueAttribute = (HasDefaultValueAttribute)Attribute.GetCustomAttribute(element, typeof(HasDefaultValueAttribute));
if (hasDefaultValueAttribute != null)
{
pvarParamDefValue = hasDefaultValueAttribute.DefaultValue;
}
}
Вызовы методов компоненты выполняются через Invoke, с ними как и с полным кодом примера можно ознакомиться в прилагаемом файле.
Таким образом, в данной библиотеке полностью отделены реализации служебных интерфейсов от логики внешней компоненты, что позволит программистам сосредоточится на выполняемых ею задачах.
к сожалению в СП написано, что подключение компоненты, созданной по технологии com, не работает на сервере.
И линукс-системы в пролете.
Но идея отличная!
В статье описана старая технология внешних компонент, которая работает только на клиенте и только на Windows. Несмотря на то, что 1С позиционирует свою открытость к интеграции, поле инструментов для создания компонент NativeAPI сильно уменьшилось, фактически остался только C++. Пример их создания от 1С с точки зрения архитектуры имеет те же недостатки, которые описаны в статье, но методы преодоления сложнее, т.к. в C++ нет отражений.
Кто-нибудь переписывал шаблон компоненты на Си (без плюсов)?
(3) на .NET тоже можно сделать компоненту NativeAPI используя Hosting CLR
To 5: Можно сделать компоненту, можно вызывать через COM+, через Web сервисы в конце концов. Но все это через проксю, иными словами в отдельном процессе, следовательно присутствуют накладные расходы на межпроцессное взаимодействие (в том числе и в отдельном AppDomain, как в Вашей компоненте). Внешние компоненты 1С как com’овские так и нативные работают в общем адресном пространстве с 1С.
(6) Я умею запускать и в основном домене, и даже использовать раннее связывание, при этом скорость значительно выше по сравнению с опубликованной компонентой, но конечно медленнее, чем компонента написанная только на с++. Но я и не утверждаю, что моя компонента и сам способ лучшие, у них единственный плюс — это простое развертывание, на сервер клиента бывает проблематично что-нибудь установить, а Native API компонента не требует установки в системе.
Я такую штуку делал в Delphi XE2, очень удобно методы подключать декораторами.
Добрый день!
Подскажите пожалуйста! Сделал сборку с тестовым классом. В целом библиотека подключается, свойства читаются, методы вызываются. Но столкнулся с непонятным поведением.
Если вызывать метод отправки внешнего события в 1С несколько раз подряд, то в 1С обработчик внешнего события вызывается только один раз для первого вызова.
Кроме того, попробовал внутри метода класса запустить таймер, который каждую секунду должен выводить сообщение в 1С и дергать внешнее событие. Методы, которые вызываются в обработчике таймера не отрабатываются в 1С, как-будто обработчик таймера вообще не срабатывает. не могу понять в чем проблема.
Показать
(10) Добрый день!
Вы явно пользуетесь не моей компонентой.
(11) Я пользуюсь библиотекой, которую скачал в этой статье с помощью большой зеленой кнопки «Скачать».
«Библиотека для создания внешних компонент на C#»
Она не Ваша?
(12) Я прошу прощения, мне вообще не стоило отвечать, это же не моя публикация))
просто уведомление пришло на почту, а я не внимательно прочитал…
Ну рабочий пример бы тут не помешал в архиве конечно.
Хотел сказать что код компоненты надо обернуть в
namespace V8.AddIn {
}
а то тип не будет виден.
В самой студии убедитесь, что у вас в Проект -> Свойства -> Сборка -> Регистрация для COM-Взаимодействия стоит галка
(10) Сообщения пропадают из-за того, что Вы скорее всего не задали размер буфера. Что касается таймеров, Вы используете объект из пространства Threads или Timers, а они многопоточные. Многозадачность порождает проблемы даже в nativeAPI-компонентах, не говоря уж о .net. Ну хочет 1С, чтобы мы использовали их асинхронные вызовы, что тут поделаешь! Попробуйте прикрутить Timer из WinForms — он singlethreaded
Подскажите, а как из кода вызвать
Состояние(<ТекстСообщения>, <Прогресс>, <Пояснение>, <Картинка>)
?
Пытаюсь создать внешнюю компоненту с помощью вашей библиотеки. 1С выдает ошибку : Тип не определен (AddIn.SomeName)
ОбъектКомпоненты = Новый(«AddIn.SomeName»);
Код компоненты:
Показать
Подскажите пожалуйста что делаю не так?
(18)
[ProgId(«AddIn.SomeComponent»)] // это имя COM-объекта, по которому Вы будете ее подключать
Оно?
(19)
нет
(20) ясно. Не там.
Там: конструктор класса пустой. В нем нужно заполнить InstanceName, тогда будет 1С видеть.
(21)
Заработало. Проблема была в том что 1С x64 была запущена.
(21) Спасибо
Добрый день, не могу понять почему 1С видит только 3 свойства
Показать