29 октября 2018 г.

Пишем подключаемый модуль на C#

У начинающих разработчиков часто появляются вопросы, как написать плагин к ЛОЦМАН Клиент на C#.

Основные проблемы составляет следующее:
  • Как экспортировать из сборки функции InitUserDLLCom, PgiCheckMenuItemCom и другие?
  • Как получить описания интерфейсов клиентского приложения?
  • Как реализовать функцию InitUserDLLCom?
  • Как разместить сборку и ее зависимости в отдельной папке?
  • Как добавить значки для пунктов меню?

Подробнее об этом ниже.

Общую информацию о создании плагинов можно почитать в статье Пишем подключаемый модуль для ЛОЦМАН Клиент.

Экспорт функций


Для экспорта функций из DLL сборки понадобятся дополнительные библиотеки. Самый простой путь это установить библиотеку Unmanaged Exports.

В меню проекта выбираем «Manage NuGet Packages», ищем и устанавливаем Unmanaged Exports.


У библиотеки Unmanaged Exports есть дальнейшее развитие github.com/3F/DllExport, но я с ним не работал.

Экспортируемые плагином функции должны выглядеть так:
public class PluginFunctions
{
    [DllExport("GetPluginInfo", CallingConvention.StdCall)]
    public static Int32 GetPluginInfo(Int32 Param, IntPtr Value)
    {
        return 0;
    }

    [DllExport("InitUserDLLCom", CallingConvention.StdCall)]
    public static Int32 InitUserDLLCom(IntPtr Value)
    {
        return 0;
    }

    [DllExport("PgiCheckMenuItemCom", CallingConvention.StdCall)]
    public static Int32 PgiCheckMenuItemCom(IntPtr Function, IntPtr IPC)
    {
        return 0;
    }

    [DllExport("ProjectList", CallingConvention.StdCall)]
    public static void ProjectList(IntPtr IPC)
    {
        return;
    }
}

Импорт описания интерфейсов клиентского приложения


В проекте выбираем «References → Add Reference», далее «COM → Type Libraries». В списке находим «Loodsman Client Library», и ставим напротив галочку.


В проект должны будут добавиться зависимости Ask, BOSimple, DataProvider, Loodsman и PDMObjects. Список может немного отличаться для разных версий ЛОЦМАН:PLM.

Для плагина понадобятся минимум две:
using DataProvider; // IDataSet
using Loodsman;     // IPluginCall

Реализация экспортируемых функций


Для реализации функции InitUserDLLCom можно пойти двумя путями: описать структуру MenuItem а затем с помощью Marshal.StructureToPtr записать ее в переданную память, либо просто скопировать байты строк в переданную память с нужным смещением используя Marshal.Copy.

Хранить меню плагина будем в массиве парами строк:
public static string[] PluginMenu = 
{
    "ProjectList",    "BEFORE_MI_TOOLS#Мои плагины#Тестовый#Список проектов",
    "LinkedFast",     "BEFORE_MI_TOOLS#Мои плагины#Тестовый#Состав" ,
};
Первый вариант со структурой MenuItem:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct MenuItem
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
    public string menu;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
    public string func;
}

[DllExport("InitUserDLLCom", CallingConvention.StdCall)]
public static Int32 InitUserDLLCom(IntPtr Value)
{
    if (Value != IntPtr.Zero)
    {
        for (int i = 0; i < PluginMenu.Length / 2; ++i)
        {
            var item = new MenuItem() { menu = PluginMenu[i * 2 + 1],
                                        func = PluginMenu[i * 2] };
            Marshal.StructureToPtr(item, Value, false);
            Value += Marshal.SizeOf(typeof(MenuItem));
        }
    }
    return PluginMenu.Length / 2;
}
Второй вариант с копированием байтов строк по нужным смещениям:
[DllExport("InitUserDLLCom", CallingConvention.StdCall)]
public static Int32 InitUserDLLCom(IntPtr Value)
{
    if (Value != IntPtr.Zero)
    {
        for (int i = 0; i < PluginMenu.Length / 2; ++i)
        {
            byte[] func = Encoding.Default.GetBytes(PluginMenu[i * 2]);
            byte[] menu = Encoding.Default.GetBytes(PluginMenu[i * 2 + 1]);
            byte[] zero = {0};
            Marshal.Copy(menu, 0, Value, menu.Length);
            Marshal.Copy(zero, 0, Value + menu.Length, zero.Length);
            Value += 255;
            Marshal.Copy(func, 0, Value, func.Length);
            Marshal.Copy(zero, 0, Value + func.Length, zero.Length);
            Value += 255;
        }
    }
    return PluginMenu.Length / 2;
}
С функцией PgiCheckMenuItemCom все просто, нужно только не забыть блок try...catch, иначе вместо сообщения об ошибке мы увидим «External exception E0434352» и, возможно, утечет память, связанная с объектом исключения.
[DllExport("PgiCheckMenuItemCom", CallingConvention.StdCall)]
public static Int32 PgiCheckMenuItemCom(IntPtr Function, IntPtr IPC)
{
    try
    {
        string func = Marshal.PtrToStringAnsi(Function);
        IPluginCall pc = (IPluginCall)Marshal.GetTypedObjectForIUnknown(IPC, typeof(IPluginCall));
        ...
        return 1;
    }
    catch (Exception)
    {
        return 0;
    }
}

Плагин и зависимости в отдельной папке


При установке плагина на рабочие места пользователей удобно, когда DLL плагина, используемые ей сборки и другие нужные файлы лежат в своей папке. Но по умолчанию поиск зависимостей будет происходить в той папке, где находится EXE-файл.

Для решения этой задачи можно добавить обработчик события AppDomain.AssemblyResolve и в нем загрузить требуемую сборку:
private static Assembly currentDomain_AssemblyResolve(object sender,
    ResolveEventArgs args)
{
    string fileName = Path.Combine(InstallPath,
        args.Name.Substring(0, args.Name.IndexOf(",")) + ".dll");
    if (File.Exists(fileName))
        return Assembly.LoadFile(fileName);
    return null;
}

Значки для пунктов меню


При необходимости, для пункта меню можно задать значок: в DLL плагина добавить ресурс типа RT_BITMAP, с именем как у экспортируемой функции.

Для этого понадобиться создать файл ресурсов PluginResources.rc и скомпилировать его в файл PluginResources.res компилятором ресурсов rc.exe из комплекта Platform SDK. Ресурсы .NET (файлы *.resx) в данном случае не подойдут.

Затем в свойствах проекта необходимо указать компилятору использовать созданный файл ресурсов PluginResources.res.


Вызов функции из автооперации Workflow


Автооперации Workflow выполняются на клиенте при движении по бизнес-процессу. Из автооперации можно вызвать как обычную функцию плагина, с помощью CallLoodsmanPlugin, так и функцию, предназначенную специально для автоопераций, с помощью ExecPluginFunction (в этом случае в функцию плагина можно передать дополнительные аргументы).

Для начала добавим в проект библиотеку типов бизнес-логики Workflow. В проекте выбираем «References → Add Reference», далее «COM → Type Libraries». В списке находим «WorkflowBusinessLogic Library», и ставим напротив галочку.

Добавляем в код:
using WorkflowBusinessLogic; // IWFBusinessLogic
Затем нужно описать прототип функции, которую будет вызывать ExecPluginFunction. К сожалению, просто использовать тип object вместо Variant у меня не получилось. Пришлось отдельно описать тип Variant в C#:
namespace PluginSampleNet
{
    [StructLayout(LayoutKind.Sequential)]
    public struct Variant
    {
        public ushort vt;
        public ushort wReserved1;
        public ushort wReserved2;
        public ushort wReserved3;
        public IntPtr data1;
        public IntPtr data2;

        public Variant(VarEnum type)
        {
            vt = (ushort)type;
            wReserved1 = 0;
            wReserved2 = 0;
            wReserved3 = 0;
            data1 = (IntPtr)0;
            data2 = (IntPtr)0;
        }

        public Variant(bool value)
        {
            vt = (ushort)VarEnum.VT_BOOL;
            wReserved1 = 0;
            wReserved2 = 0;
            wReserved3 = 0;
            data1 = (IntPtr)Convert.ToInt32(value);
            data2 = (IntPtr)0;
        }

        public Variant(int value)
        {
            vt = (ushort)VarEnum.VT_I4;
            wReserved1 = 0;
            wReserved2 = 0;
            wReserved3 = 0;
            data1 = (IntPtr)value;
            data2 = (IntPtr)0;
        }

        public Variant(string value)
        {
            vt = (ushort)VarEnum.VT_BSTR;
            wReserved1 = 0;
            wReserved2 = 0;
            wReserved3 = 0;
            data1 = (IntPtr)UnsafeNativeMethods.SysAllocString(value);
            data2 = (IntPtr)0;
        }
    }
}
Описание структуры TPDMVersionData:
namespace PluginSampleNet
{
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct PDMVersionData
    {
        [MarshalAs(UnmanagedType.I4)]
        public int routeId;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
        public string appServer;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
        public string db;
        [MarshalAs(UnmanagedType.I4)]
        public int id;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
        public string product;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
        public string type;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
        public string version;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
        public string state;
        [MarshalAs(UnmanagedType.I1)]
        public byte accessLevel;
        [MarshalAs(UnmanagedType.I1)]
        public byte lockLevel;
        [MarshalAs(UnmanagedType.I1)]
        public byte document;
        [MarshalAs(UnmanagedType.I1)]
        public byte revision;
    }
}
И прототип функции:
namespace PluginSampleNet
{
    public class PluginFunctions
    {
        public static bool WFProjectList(IWFBusinessLogic wf, 
            PDMVersionData versionData, object[] userData)
        {
            return true;
        }

        [DllExport("WFProjectList", CallingConvention.StdCall)]
        public static Variant _wfProjectList(Variant wfo, 
            IntPtr versionData, object[] userData)
        {
            if (!_initialized)
            {
                Initialize();
                _initialized = true;
            }
            try
            {
                if (wfo.vt != (ushort)VarEnum.VT_DISPATCH) 
                { 
                    throw new ArgumentException();
                }
                IWFBusinessLogic wf = 
                    (IWFBusinessLogic)Marshal.GetTypedObjectForIUnknown(wfo.data1, 
                        typeof(IWFBusinessLogic));
                PDMVersionData data = 
                    (PDMVersionData)Marshal.PtrToStructure(versionData, 
                        typeof(PDMVersionData));
                bool res = WFProjectList(wf, data, userData);
                return new Variant(res);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), PluginCaption, 
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
                return new Variant(VarEnum.VT_NULL);
            }
        }
    }
}

Исходный код


Исходный код примера плагина доступен на GitHub — github.com/achechulin/loodsman/Plugins/PluginSampleNet.

Текущую версию кода можно скачать без клиента Git или SVN, нажав кнопку «Download ZIP» в правой части страницы репозитория.

7 комментариев:

  1. Привет) А не подскажешь как аргумент передать например в функцию в ProjectList. Я пишу автоматическую операцию в workflow и использую функцию из плагина на c#, функция вызывается, а вот как аргумент достать не пойму..

    ОтветитьУдалить
    Ответы
    1. С помощью какой функции вы вызываете ваш плагин из автооперации (CallLoodsmanPlugin/ExecPluginFunction/ExecDllFunction)?

      Удалить
  2. Я использую ExecPluginFunction, в нее передаю 3 параметром текстовое значение, а в dll хочу его вытащить.

    Не понимаю, что здесь надо написать.
    [DllExport("ProjectList", CallingConvention.StdCall)]
    public static void ProjectList(IntPtr IPC)
    {
    return;
    }

    ОтветитьУдалить
  3. В методе public static Variant _wfProjectList последним параметром передается массив переменных, однако, как бы я не заполнял этот массив в автоматической операции перед вызовом метода, в управляемом коде массив userData содержит один элемент - первый из переданных.

    Пример вызова в автоматической операции:

    var a: array of OleVariant;
    begin
    a := [1,2];
    res:=ExecPluginFunction('..\WorkflowPlugin.dll','InvokeDynaMethod', a);

    ОтветитьУдалить
  4. Вот так надо, чтобы получить весь массив переданный, а не только первый элемент.
    [DllExport("WFProjectList", CallingConvention.StdCall)]
    public static Variant _wfProjectList(Variant wfo,
    IntPtr versionData, IntPtr userData)
    Marshal.GetObjectsForNativeVariants(userData, размерность массива)

    ОтветитьУдалить