Основные проблемы составляет следующее:
- Как экспортировать из сборки функции
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» в правой части страницы репозитория.
Привет) А не подскажешь как аргумент передать например в функцию в ProjectList. Я пишу автоматическую операцию в workflow и использую функцию из плагина на c#, функция вызывается, а вот как аргумент достать не пойму..
ОтветитьУдалитьС помощью какой функции вы вызываете ваш плагин из автооперации (CallLoodsmanPlugin/ExecPluginFunction/ExecDllFunction)?
УдалитьЯ использую ExecPluginFunction, в нее передаю 3 параметром текстовое значение, а в dll хочу его вытащить.
ОтветитьУдалитьНе понимаю, что здесь надо написать.
[DllExport("ProjectList", CallingConvention.StdCall)]
public static void ProjectList(IntPtr IPC)
{
return;
}
Дополнил статью.
УдалитьВ методе public static Variant _wfProjectList последним параметром передается массив переменных, однако, как бы я не заполнял этот массив в автоматической операции перед вызовом метода, в управляемом коде массив userData содержит один элемент - первый из переданных.
ОтветитьУдалитьПример вызова в автоматической операции:
var a: array of OleVariant;
begin
a := [1,2];
res:=ExecPluginFunction('..\WorkflowPlugin.dll','InvokeDynaMethod', a);
[DllExport("Test", CallingConvention.StdCall)]
Удалитьpublic static Variant Test(Variant wfo, IntPtr versionData, IntPtr userDataPtr, int userDataUpperBound)
{
try
{
if (wfo.vt != (ushort)VarEnum.VT_DISPATCH)
throw new ArgumentException();
var wf = (IWFBusinessLogic)Marshal.GetTypedObjectForIUnknown(wfo.data1, typeof(IWFBusinessLogic));
var data = (PDMVersionData)Marshal.PtrToStructure(versionData, typeof(PDMVersionData));
object[] userData;
if (userDataPtr != IntPtr.Zero)
userData = Marshal.GetObjectsForNativeVariants(userDataPtr, userDataUpperBound + 1);
else
userData = new object[0];
bool result = Test(wf, data, userData);
return new Variant(result);
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), "WFPluginError", MessageBoxButtons.OK, MessageBoxIcon.Error);
return new Variant(VarEnum.VT_NULL);
}
}
Аналогично
ОтветитьУдалитьВот так надо, чтобы получить весь массив переданный, а не только первый элемент.
ОтветитьУдалить[DllExport("WFProjectList", CallingConvention.StdCall)]
public static Variant _wfProjectList(Variant wfo,
IntPtr versionData, IntPtr userData)
Marshal.GetObjectsForNativeVariants(userData, размерность массива)
Чтобы нормально передавались строки в PDMVersionData, пишем в атрибуте юникод:
ОтветитьУдалить[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct PDMVersionData
...
Чтобы нормально получать массив с userData, берем и маршалингом получаем весь массив
[DllExport("Test", CallingConvention.StdCall)]
public static Variant Test(Variant wfo, IntPtr versionData, IntPtr userDataPtr, int userDataUpperBound)
{
try
{
if (wfo.vt != (ushort)VarEnum.VT_DISPATCH)
throw new ArgumentException();
var wf = (IWFBusinessLogic)Marshal.GetTypedObjectForIUnknown(wfo.data1, typeof(IWFBusinessLogic));
var data = (PDMVersionData)Marshal.PtrToStructure(versionData, typeof(PDMVersionData));
object[] userData;
if (userDataPtr != IntPtr.Zero)
userData = Marshal.GetObjectsForNativeVariants(userDataPtr, userDataUpperBound + 1);
else
userData = new object[0];
bool result = Test(wf, data, userData);
return new Variant(result);
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), "WFPluginError", MessageBoxButtons.OK, MessageBoxIcon.Error);
return new Variant(VarEnum.VT_NULL);
}
}
Юникодные строки в PDMVersionData появились только в версии 22.2 Лоцмана.
УдалитьЗдравствуйте, отличная статья, очень помогла! А Вы не знаете как добавить иконку для плагина с использованием интерфейса ILoodsmanNetPlugin? Ваш способ с nuget пакетом DllExport работает, но самостоятельно добавить иконку через новый способ не получается, я называю иконку в ресурсах также как и метод в menu.AddMenuItem("Icon", Icon, CheckIcon).
ОтветитьУдалить