gifts2017

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

Опубликовал Игорь Кисиль (IgorKissil) в раздел Программирование - Внешние компоненты

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

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

Скачать файлы

Наименование Файл Версия Размер Кол. Скачив.
Библиотека для создания внешних компонент на C#
.zip 9,60Kb
28.02.15
87
.zip 9,60Kb 87 Скачать

См. также

Подписаться Добавить вознаграждение

Комментарии

1. Игорь <...> (I_G_O_R) 02.03.15 23:50
к сожалению в СП написано, что подключение компоненты, созданной по технологии com, не работает на сервере.
2. Никита Грызлов (nixel) 04.03.15 11:24
И линукс-системы в пролете.
Но идея отличная!
3. Игорь Кисиль (IgorKissil) 04.03.15 11:57
В статье описана старая технология внешних компонент, которая работает только на клиенте и только на Windows. Несмотря на то, что 1С позиционирует свою открытость к интеграции, поле инструментов для создания компонент NativeAPI сильно уменьшилось, фактически остался только C++. Пример их создания от 1С с точки зрения архитектуры имеет те же недостатки, которые описаны в статье, но методы преодоления сложнее, т.к. в C++ нет отражений.
4. Александр Топольский (AlexanderKai) 16.04.15 13:43
Кто-нибудь переписывал шаблон компоненты на Си (без плюсов)?
5. Игорь <...> (I_G_O_R) 08.05.15 13:59
(3) IgorKissil, на .NET тоже можно сделать компоненту NativeAPI используя Hosting CLR
6. Игорь Кисиль (IgorKissil) 04.08.15 06:52
To 5: Можно сделать компоненту, можно вызывать через COM+, через Web сервисы в конце концов. Но все это через проксю, иными словами в отдельном процессе, следовательно присутствуют накладные расходы на межпроцессное взаимодействие (в том числе и в отдельном AppDomain, как в Вашей компоненте). Внешние компоненты 1С как com'овские так и нативные работают в общем адресном пространстве с 1С.
7. Игорь <...> (I_G_O_R) 04.08.15 19:40
(6) Я умею запускать и в основном домене, и даже использовать раннее связывание, при этом скорость значительно выше по сравнению с опубликованной компонентой, но конечно медленнее, чем компонента написанная только на с++. Но я и не утверждаю, что моя компонента и сам способ лучшие, у них единственный плюс - это простое развертывание, на сервер клиента бывает проблематично что-нибудь установить, а Native API компонента не требует установки в системе.
8. Евгений Стоянов (quick) 20.10.15 14:38
Я такую штуку делал в Delphi XE2, очень удобно методы подключать декораторами.
9. Сергей Смирнов (Serginio) 29.12.15 14:32
Работая с Глобальным контекстом проще работать через DynamicObject

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Dynamic;
using System.Reflection;
using System.Runtime.InteropServices;

namespace ТестВК
{

    class ДинамикГК : DynamicObject, IDisposable

{
        dynamic ГК1С;
        Type ТипГК;
        object App1C;

        void УничтожитьОбъект(object Объект)
        {
            Marshal.Release(Marshal.GetIDispatchForObject(Объект));
            Marshal.ReleaseComObject(Объект);


        }
  public ДинамикГК(dynamic ГК1С)
        {
            ТипГК = ГК1С.GetType();
            App1C = ГК1С.AppDispatch;
            this.ГК1С = ГК1С;


        }
    // установка свойства
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
         try
        {
        ТипГК.InvokeMember(binder.Name, BindingFlags.SetProperty, null, App1C, new object[]{value});
        return true;
        }
         catch (Exception)
         {
         }
         return false;
    }
    // получение свойства
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        try
        {
            result = ТипГК.InvokeMember(binder.Name, BindingFlags.GetProperty, null, App1C, null);
            return true;
        }
        catch (Exception)
        {  
        }
        result = null;
        return false;
    }
    // вызов метода
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
       // dynamic method = members[binder.Name];
       // result = method((int)args[0]);
       // return result != null;
        if (binder.Name=="ЗакрытьОбъект")
        {

            Dispose();
            result = null;
            return true;

        }


        try
        {
            if (args.Length == 1 && args[0].GetType() == typeof(System.Object[]))
                result = ТипГК.InvokeMember(binder.Name, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod, null, App1C, (System.Object[])args[0]);
            else
                result = ТипГК.InvokeMember(binder.Name, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod, null, App1C, args);

            return true;
        }
        catch (Exception)
        {
        }
        result = null;
        return false;
    }

    bool disposed = false;
      public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    // Protected implementation of Dispose pattern. 
    protected virtual void Dispose(bool disposing)
    {
        if (disposed)
            return;

        if (disposing)
        {
            if (ГК1С != null)
            {
                УничтожитьОбъект(ГК1С);
                УничтожитьОбъект(App1C);
                ГК1С = null;
                App1C = null;
                // Free any other managed objects here. 
                //
            }
        }

        // Free any unmanaged objects here. 
        //
        disposed = true;
    }

  }


}
...Показать Скрыть


И использование


 public dynamic Новый( params object[] Параметры)
    {

        return ГК.NewObject(Параметры);
    }



ГК = new ДинамикГК(Object1C);

 ГК.Сообщить("Привет из ВК", ГК.СтатусСообщения.Важное);


dynamic Тз = Новый("ТаблицаЗначений");
    dynamic Колонки = Тз.Колонки;
    Колонки.Добавить("КолонкаЧисло", ПолучитьОписаниеТиповЧисла(9, 0));
    Колонки.Добавить("КолонкаЧисло4", ПолучитьОписаниеТиповЧисла(7, 2));

...Показать Скрыть


Подробне здесь http://infostart.ru/public/238584/
Для написания сообщения необходимо авторизоваться
Прикрепить файл
Дополнительные параметры ответа