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.


Исходный код


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

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

Комментариев нет:

Отправить комментарий