Библиотека классов для создания внешней компоненты 1С на C#

В статье предложен набор классов-оберток над служебными интерфейсами 1С:Предприятия, позволяющий реализовать внешнюю компоненту в виде обычного класса .NET

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, с ними как и с полным кодом примера можно ознакомиться в прилагаемом файле.

Таким образом, в данной библиотеке полностью отделены реализации служебных интерфейсов от логики внешней компоненты, что позволит программистам сосредоточится на выполняемых ею задачах.

24 Comments

  1. I_G_O_R

    к сожалению в СП написано, что подключение компоненты, созданной по технологии com, не работает на сервере.

    Reply
  2. nixel

    И линукс-системы в пролете.

    Но идея отличная!

    Reply
  3. IgorKissil

    В статье описана старая технология внешних компонент, которая работает только на клиенте и только на Windows. Несмотря на то, что 1С позиционирует свою открытость к интеграции, поле инструментов для создания компонент NativeAPI сильно уменьшилось, фактически остался только C++. Пример их создания от 1С с точки зрения архитектуры имеет те же недостатки, которые описаны в статье, но методы преодоления сложнее, т.к. в C++ нет отражений.

    Reply
  4. AlexanderKai

    Кто-нибудь переписывал шаблон компоненты на Си (без плюсов)?

    Reply
  5. I_G_O_R

    (3) на .NET тоже можно сделать компоненту NativeAPI используя Hosting CLR

    Reply
  6. IgorKissil

    To 5: Можно сделать компоненту, можно вызывать через COM+, через Web сервисы в конце концов. Но все это через проксю, иными словами в отдельном процессе, следовательно присутствуют накладные расходы на межпроцессное взаимодействие (в том числе и в отдельном AppDomain, как в Вашей компоненте). Внешние компоненты 1С как com’овские так и нативные работают в общем адресном пространстве с 1С.

    Reply
  7. I_G_O_R

    (6) Я умею запускать и в основном домене, и даже использовать раннее связывание, при этом скорость значительно выше по сравнению с опубликованной компонентой, но конечно медленнее, чем компонента написанная только на с++. Но я и не утверждаю, что моя компонента и сам способ лучшие, у них единственный плюс — это простое развертывание, на сервер клиента бывает проблематично что-нибудь установить, а Native API компонента не требует установки в системе.

    Reply
  8. quick

    Я такую штуку делал в Delphi XE2, очень удобно методы подключать декораторами.

    Reply
  9. Serginio
    Reply
  10. itworks

    Добрый день!

    Подскажите пожалуйста! Сделал сборку с тестовым классом. В целом библиотека подключается, свойства читаются, методы вызываются. Но столкнулся с непонятным поведением.

    Если вызывать метод отправки внешнего события в 1С несколько раз подряд, то в 1С обработчик внешнего события вызывается только один раз для первого вызова.

    Кроме того, попробовал внутри метода класса запустить таймер, который каждую секунду должен выводить сообщение в 1С и дергать внешнее событие. Методы, которые вызываются в обработчике таймера не отрабатываются в 1С, как-будто обработчик таймера вообще не срабатывает. не могу понять в чем проблема.

    namespace V8.AddIn
    {
    public class MyTest1C
    {
    public MyTest1C()
    {
    this.InstanceName = «MyTest1C»;
    }
    
    private Timer timer;
    
    [Alias(«Включена»)]
    public bool IsEnabled { get; set; }
    
    [Alias(«ИмяЭкземпляра»)]
    public string InstanceName { get; private set; }
    
    [Alias(«Выполнить»)]
    public void Make(string How, int Count)
    {
    if (IsEnabled)
    {
    timer = new Timer(1000);
    timer.AutoReset = true;
    timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
    timer.Start();
    
    V8Context.CreateV8Context().V8Message(MessageTypes.Info, «Hello!»); // Работает
    V8Context.CreateV8Context().V8Message(MessageTypes.Info, «Hello!»); // Работает
    V8Context.CreateV8Context().AsyncEvent.ExternalEvent(«Test1», «Test2», «Test3»); // Работает
    V8Context.CreateV8Context().AsyncEvent.ExternalEvent(«Test1», «Test2», «Test3»); // Не работает
    }
    }
    
    void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
    V8Context.CreateV8Context().V8Message(MessageTypes.Info, «Hello!»);  // Не работает
    V8Context.CreateV8Context().AsyncEvent.ExternalEvent(«Test1», «Test2», «Test3»); // Не работает
    }
    }
    }
    

    Показать

    Reply
  11. I_G_O_R

    (10) Добрый день!

    Вы явно пользуетесь не моей компонентой.

    Reply
  12. itworks

    (11) Я пользуюсь библиотекой, которую скачал в этой статье с помощью большой зеленой кнопки «Скачать».

    «Библиотека для создания внешних компонент на C#»

    Она не Ваша?

    Reply
  13. I_G_O_R

    (12) Я прошу прощения, мне вообще не стоило отвечать, это же не моя публикация))

    просто уведомление пришло на почту, а я не внимательно прочитал…

    Reply
  14. twin

    Ну рабочий пример бы тут не помешал в архиве конечно.

    Reply
  15. twin

    Хотел сказать что код компоненты надо обернуть в

    namespace V8.AddIn {

    }

    а то тип не будет виден.

    В самой студии убедитесь, что у вас в Проект -> Свойства -> Сборка -> Регистрация для COM-Взаимодействия стоит галка

    Reply
  16. IgorKissil

    (10) Сообщения пропадают из-за того, что Вы скорее всего не задали размер буфера. Что касается таймеров, Вы используете объект из пространства Threads или Timers, а они многопоточные. Многозадачность порождает проблемы даже в nativeAPI-компонентах, не говоря уж о .net. Ну хочет 1С, чтобы мы использовали их асинхронные вызовы, что тут поделаешь! Попробуйте прикрутить Timer из WinForms — он singlethreaded

    Reply
  17. twin

    Подскажите, а как из кода вызвать

    Состояние(<ТекстСообщения>, <Прогресс>, <Пояснение>, <Картинка>)

    ?

    Reply
  18. _Devill

    Пытаюсь создать внешнюю компоненту с помощью вашей библиотеки. 1С выдает ошибку : Тип не определен (AddIn.SomeName)

    ОбъектКомпоненты = Новый(«AddIn.SomeName»);

    Код компоненты:

    using System;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using V8.AddIn;
    
    namespace V8.AddIn
    {
    
    [ComVisible(true)]
    [Guid(«9fe0db43-beef-4358-b46a-1fb0f80c9bd6»)] // произвольный Guid-идентификатор Вашей компоненты
    [ProgId(«AddIn.SomeComponent»)] // это имя COM-объекта, по которому Вы будете ее подключать
    public class Some : LanguageExtenderAddIn
    {
    public Some() : base(typeof(SomeName), 1000) { }
    }
    
    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; }
    
    }
    
    
    }

    Показать

    Подскажите пожалуйста что делаю не так?

    Reply
  19. spacecraft

    (18)

    ОбъектКомпоненты = Новый(«AddIn.SomeName»);

    [ProgId(«AddIn.SomeComponent»)] // это имя COM-объекта, по которому Вы будете ее подключать

    Оно?

    Reply
  20. _Devill

    (19)

    нет

    ПодключитьВнешнююКомпоненту(«AddIn.SomeComponent»);
    ОбъектКомпоненты = Новый(«AddIn.SomeName»);
    Reply
  21. spacecraft

    (20) ясно. Не там.

    Там: конструктор класса пустой. В нем нужно заполнить InstanceName, тогда будет 1С видеть.

    public SomeName()
    {
    InstanceName = «SomeName»;
    }
    Reply
  22. _Devill

    (21)

    Заработало. Проблема была в том что 1С x64 была запущена.

    Reply
  23. _Devill

    (21) Спасибо

    Reply
  24. _Devill

    Добрый день, не могу понять почему 1С видит только 3 свойства

    using System;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Net;
    using System.Net.Sockets;
    using System.Text.RegularExpressions;
    using System.Text;
    using System.Threading;
    using System.IO;
    using V8.AddIn;
    
    
    namespace V8.AddIn
    {
    public class SomeName
    {
    private Socket clientSocket;
    
    [Alias(«Пароль»)]
    public string Password { get; set; }
    [Alias(«IPАдрес»)]
    public string IPAdress { get; set; }
    [Alias(«Порт»)]
    public int Port { get; set; }
    [Alias(«ИмяПользователя»)]
    public string Name { get; set; }
    
    
    [Alias(«Подключиться»)]
    public bool Connection()
    {
    
    
    clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(IPAdress), Port);
    clientSocket.Connect(serverEndPoint);
    
    // Login to the server; manager.conf needs to be setup with matching credentials.
    clientSocket.Send(Encoding.ASCII.GetBytes(«Action: Login
    Username: «+ Name + »
    Secret: «+ Password + »
    ActionID: 1
    Events: off
    Eventmask: call
    
    «));
    
    int bytesRead = 0;
    byte[] buffer = new byte[1024];
    bytesRead = clientSocket.Receive(buffer);
    string response = «»;
    response = Encoding.ASCII.GetString(buffer, 0, bytesRead);
    
    if (Regex.Match(response, «Message: Authentication accepted», RegexOptions.IgnoreCase).Success)
    {
    // Send a ping request the asterisk server will send back a pong response.
    return true;
    
    } else
    {
    return false;
    }
    
    }
    public SomeName()
    {
    
    }
    }
    
    [ComVisible(true)]
    [Guid(«9fe0db43-beef-4358-b46a-1fb0f80c9bd6»)] // произвольный Guid-идентификатор Вашей компоненты
    [ProgId(«AddIn.Some»)] // это имя COM-объекта, по которому Вы будете ее подключать
    public class Some : LanguageExtenderAddIn
    {
    public Some() : base(typeof(SomeName), 1000) { }
    }
    }

    Показать

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *