23.11.2020, 23:29 | #1 |
Administrator
|
D365FO: Отображение в контекстном меню названия поля / метода таблицы
Добрый день всем!
Очень не хватает в D365FO (после всех версий AX) получить информацию в пользовательском режиме от контрола - к какому полю / метода какого датасорса (с указанием названия таблицы / представления). По сравнению с предыдущими версиями AX получение информации усложняется потенциальным наличием пользовательских контролов (по типу DimensionEntryControl), которые описываются обычными классами на X++. Вдобавок немного поменялся подход к рисованию контекстных меню. Решил попробовать сделать нечто подобное. Пока, к сожалению, нет возможности масштабно обкатать модификацию, поэтому прошу сообщать о выявленных ошибках и ситуациях. Итак, хочется получить вот такие вот картинки: Попутно я решил попробовать написать код так, чтобы добавление новых пользовательских контролов не привело бы к необходимости править мой код. Поэтому в программный код были добавлены делегаты и обработка пользовательских контролов осуществляется путем подписки на 3 делегата: - делегат, который определяет, что выбранный контрол является пользовательским - делегат, который вычисляет название датасорса с таблицей - делегат, который вычисляет название поля Образец написания подписчиков делегатов представлен на примере обработки контрола DimensionEntryControl для финансовых аналитик. Поддерживаемые типы контролов:
Технически, система при открытии формы обходит все поддерживаемые контролы и добавляет в их контекстные меню - дополнительные пункты меню. Само собой - это заметно сказывается как на времени открытия формы, так и на времени отображения контекстного меню. Поэтому в параметрах пользователя предусмотрен флажок (по умолчанию выключенный), который включает данную функциональность для конкретного пользователя. Код постарался подробно прокомментировать (умещается всё в одном классе) X++: // VSUH, 23.11.2020 Добавление названий источника данных (датасорс + поле / метод) в контекстное меню контрола на форме class FormControlShowDevInfo { public FormRun formRun; // Обрабатываемая форма const int cInfoDataSource = -100; // Код пункта меню с названием датасорса и таблицы const int cInfoFieldMethod = -101; // Код пункта меню с названием поля / метода таблицы #Properties public const str cPropertyDataSource = #PropertyDataSource; public const str cPropertyDataFieldName = #PropertyDataFieldName; // Map для хранения контекстного меню, которое может быть переопределено разработчиком на форме. // Если меню не переопределено разработчиком, то значение в Map пустое. Попутно выполняет роль перечня контролов, // у которых переопределено контекстное меню данным классом (т.к. нельзя 2 раза вызвать метод registerOverride) Map ctrlContextMenu; // Map для хранения контролов, которые были созданы в контейнере пользовательского (Custom) контрола. // Например, в контроле финансовых аналитик (DimensionEntryControl) в момент запуска формы добавляются FormStringControl-ы // для вывода непосредственно значений аналитик. В данном Map хранятся эти FormStringControl-ы, с привязкой к родительскому // пользовательскому контролу (он хранится в value). Все дочерние контролы автоматически получают информацию об источнике // данных от своего родительского пользовательского контрола (в случае финансовых аналитик - DimensionEntryControl) Map ctrlChildCustomControl; // Родительский пользовательский контрол, если производится обход подчиненных контролов (например, для финансовых аналитик - // это DimensionEntryControl) FormContainerControl parentCustomCtrl; /// <summary> /// "Точка входа" в функционал. Метод вызывается после вызова super() в методе formRun.run() /// </summary> /// <param name = "_formInstance"> /// Объект открывшейся формы /// </param> [SubscribesTo(classStr(FormRun), staticDelegateStr(FormRun, onFormRunCompleted))] public static void FormRun_onFormRunCompleted(FormRun _formInstance) { if (SysUserInfo::find().ShowFormControlDevInfo) { FormControlShowDevInfo::instance(_formInstance).run(); } } /// <summary> /// Параметр для сохранения класса в глобальном кэше /// </summary> /// <param name = "_formRun"></param> /// <returns></returns> public static str globalCacheOwner(FormRun _formRun) { return strFmt("Form:%1", _formRun.name()); } /// <summary> /// Параметр для сохранения класса в глобальном кэше /// </summary> /// <param name = "_formRun"></param> /// <returns></returns> public static int globalCacheKey() { return classNum(FormControlShowDevInfo); } /// <summary> /// Класс запускается в режиме "singleton", т.е. для каждой формы - свой единственный экземпляр. Запущенный экземпляр сохраняется в глобальном кэше /// </summary> /// <param name = "_formRun"> /// Экземпляр формы /// </param> /// <returns></returns> public static FormControlShowDevInfo instance(FormRun _formRun) { FormControlShowDevInfo runClass; if (appl.globalCache().isSet(FormControlShowDevInfo::globalCacheOwner(_formRun), FormControlShowDevInfo::globalCacheKey())) { runClass = appl.globalCache().get(FormControlShowDevInfo::globalCacheOwner(_formRun), FormControlShowDevInfo::globalCacheKey()); } else { runClass = new FormControlShowDevInfo(); runClass.formRun = _formRun; appl.globalCache().set(FormControlShowDevInfo::globalCacheOwner(_formRun), FormControlShowDevInfo::globalCacheKey(), runClass); } return runClass; } private void initCtrlContextMenuMap() { ctrlContextMenu = new Map(Types::Class, Types::String); ctrlChildCustomControl = new Map(Types::Class, Types::Class); } /// <summary> /// Стартовый метод запуска обработки формы /// </summary> public void run() { this.setContextMenuOnCtrl(formRun.design()); } /// <summary> /// Метод, определяющий тип контрола. У данного типа контрола свойство, содержащее в себе код поля называется /// referenceField (в отличии от контролов, перечисленных в методе isSimpleControl()) /// У всех контролов обязательно должен быть метод registerOverrideMethod /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <returns> /// Возвращает истину, если контрол принадлежит к одному из типов, у которых есть метод referenceField /// </returns> public boolean isReferenceControl(FormControl _ctrl) { boolean ret; switch (classIdGet(_ctrl)) { case classNum(FormReferenceGroupControl): case classNum(FormSegmentedEntryControl): case classNum(SegmentedEntryControl): ret = true; break; } return ret; } /// <summary> /// Метод, определяющий тип контрола. У данного типа контрола свойство, содержащее в себе код поля называется /// dataField (в отличии от контролов, перечисленных в методе isReferenceControl()) /// У всех контролов обязательно должен быть метод registerOverrideMethod /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <returns> /// Возвращает истину, если контрол принадлежит к одному из типов, у которых есть метод dataField /// </returns> public boolean isSimpleControl(FormControl _ctrl) { boolean ret; switch (classIdGet(_ctrl)) { case classNum(FormCheckBoxControl): case classNum(FormComboBoxControl): case classNum(FormDateControl): case classNum(FormDateTimeControl): case classNum(FormGuidControl): case classNum(FormInt64Control): case classNum(FormIntControl): case classNum(FormRadioControl): case classNum(FormRealControl): case classNum(FormRichTextControl): case classNum(FormStaticTextControl): case classNum(FormStringControl): case classNum(FormTimeControl): case classNum(FormWindowControl): ret = true; break; } return ret; } /// <summary> /// Метод, определяющий тип контрола. Данный контрол является группирующим, т.е. у него есть дочерние контролы, /// которые можно перебрать, используя методы controlCount() и controlNum() /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <returns> /// Возвращает истину, если контрол является группирующим /// </returns> public boolean isGroupControl(FormControl _ctrl) { boolean ret; switch (classIdGet(_ctrl)) { case classNum(FormGroupControl): case classNum(FormGridControl): case classNum(FormTabControl): case classNum(FormTabPageControl): ret = true; break; } return ret; } /// <summary> /// Метод, определяющий тип контрола. Данный контрол является пользовательским, т.е. он базируется на обычном /// классе из АОТ из узла \Code\Classes. Пользовательских контролов может быть много, поэтому добавление обработки /// нового пользовательского контрола осуществляется через делегаты /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <returns> /// Возвращает истину, если контрол является пользовательским /// </returns> public boolean isCustomControl(FormControl _ctrl) { boolean ret; FormRunServiceArgs isStdCtrl = new FormRunServiceArgs(); this.checkIsCustomControl(_ctrl, isStdCtrl); ret = isStdCtrl.cancelled(); return ret; } /// <summary> /// Обход контролов на форме и переопределение метода getContextMenuOptions для добавления в контекстное меню пунктов /// </summary> /// <param name = "_groupControls"> /// Группирующий контрол /// </param> private void setContextMenuOnCtrl(Object _groupControls) { Object itemControl; boolean canOverride; ; for (int i = 1; i <= _groupControls.controlCount(); i++) { itemControl = _groupControls.controlNum(i); // Рекурсия, если контрол группирующий if (this.isGroupControl(itemControl)) { this.setContextMenuOnCtrl(itemControl); } canOverride = (this.getInfoDataSourceStr(itemControl) && (this.getInfoFieldStr(itemControl) || this.getInfoMethodStr(itemControl))) || parentCustomCtrl; // Переопределение контекстного меню выполняется только в случае, если контрол привязан к полю или методу. Либо является // подчиненным контролом пользовательского контрола (например FormStringControl в контроле DimensionEntryControl) // Т.о. для несвязанных (Unbound) контролов контекстное меню не переопределяется if (canOverride) { if (!ctrlContextMenu) { this.initCtrlContextMenuMap(); } if (!ctrlContextMenu.exists(itemControl)) { ctrlContextMenu.insert(itemControl, itemControl.getContextMenuOptions()); if (parentCustomCtrl) { ctrlChildCustomControl.insert(itemControl, parentCustomCtrl); } itemControl.registerOverrideMethod(methodStr(FormControl, getContextMenuOptions), methodStr(FormControlShowDevInfo, getContextMenuOptions), FormControlShowDevInfo::instance(formRun)); // Анализируются подчиненные контролы пользовательского контрола только в случае, если пользовательский контрол отнаследован от класса FormContainerControl if (this.isCustomControl(itemControl) && SysDictClass::isEqualOrSuperclass(classIdGet(itemControl), classNum(FormContainerControl))) { parentCustomCtrl = itemControl; this.setContextMenuOnCtrl(itemControl); parentCustomCtrl = null; } } } } } /// <summary> /// Получение ссылки на датасорс формы (FormDataSource) на основе переданного контрола. Для контролов, являющихся /// подчиненными контролами пользовательского контрола - возвращается ссылка на датасорс пользовательского контрола /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <returns> /// Ссылка на датасорс формы. Если ссылку определить не удалось - вернется null /// </returns> private FormDataSource getDataSource(Object _ctrl) { FormDataSource formDS; // Для контролов из методов isSimpleControl и isReferenceControl датасорс определяется по идентификатору if (this.isSimpleControl(_ctrl) || this.isReferenceControl(_ctrl)) { if (ctrlChildCustomControl && ctrlChildCustomControl.exists(_ctrl)) { FormContainerControl parentCustomControl = ctrlChildCustomControl.lookup(_ctrl); return this.getDataSource(parentCustomControl); } if (_ctrl.dataSource()) { for (int i = 1; i <= formRun.dataSourceCount(); i++) { if (formRun.dataSource(i).id() == _ctrl.dataSource()) { formDS = formRun.dataSource(i); break; } } } } // В пользовательских контролах заранее неизвестно, какой метод возвращает название датасорса. // Поэтому конкретный метод определяется в делегате getDataSourceCustomControl, а значение этого метода сохраняется в // экземпляре класса FormProperty под названием, которое определено в константе cPropertyDataSource if (this.isCustomControl(_ctrl)) { FormPropertySet propertySet = new FormPropertySet(); this.getDataSourceCustomControl(_ctrl, propertySet); FormProperty formProperty = propertySet.getProperty(FormControlShowDevInfo::cPropertyDataSource); if (formProperty) { str dsName = formProperty.parmValue(); if (dsName) { formDS = formRun.dataSource(dsName); } } } return formDS; } /// <summary> /// Формирование строки с названием датасорса и его таблицы для контрола /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <returns> /// Текстовая строка в формате <Название датасорса> (<Название таблицы / view>) /// </returns> private str getInfoDataSourceStr(Object _ctrl) { FormDataSource formDS = this.getDataSource(_ctrl); str infoStr; if (formDS) { infoStr = strFmt("%1 (%2)", formDS.name(), tableId2Name(formDS.table())); } return infoStr; } /// <summary> /// Формирование строки с названием поля для контрола. Для контролов, являющихся подчиненными контролами /// пользовательского контрола - возвращается название поля пользовательского контрола /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <returns> /// Текстовая строка с названием поля /// </returns> private str getInfoFieldStr(Object _ctrl) { FormDataSource formDS = this.getDataSource(_ctrl); str infoStr; if (formDS) { if (ctrlChildCustomControl && ctrlChildCustomControl.exists(_ctrl)) { FormContainerControl parentCustomControl = ctrlChildCustomControl.lookup(_ctrl); return this.getInfoFieldStr(parentCustomControl); } if (this.isSimpleControl(_ctrl)) { if (_ctrl.dataField()) { infoStr = fieldId2Name(formDS.table(), _ctrl.dataField()); } } if (this.isReferenceControl(_ctrl)) { if (_ctrl.referenceField()) { infoStr = fieldId2Name(formDS.table(), _ctrl.referenceField()); } } // В пользовательских контролах заранее неизвестно, какой метод возвращает название поля. // Поэтому конкретный метод определяется в делегате getFieldNameCustomControl, а значение этого метода сохраняется в // экземпляре класса FormProperty под названием, которое определено в константе cPropertyDataFieldName if (this.isCustomControl(_ctrl)) { FormPropertySet propertySet = new FormPropertySet(); this.getFieldNameCustomControl(_ctrl, propertySet); FormProperty formProperty = propertySet.getProperty(FormControlShowDevInfo::cPropertyDataFieldName); if (formProperty) { infoStr = formProperty.parmValue(); } } } return infoStr; } /// <summary> /// Формирование строки с названием метода для контрола. Для контролов, являющихся подчиненными контролами /// пользовательского контрола - возвращается название метода пользовательского контрола /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <returns> /// Текстовая строка с названием метода. Для расширений в этой текстовой строке будет представлено выражение, /// содержащее в себе название класса-расширения и название его метода (либо в формате <класс-расширение>.<метод>, /// если разработчик использует Chain Of Command, либо в формате <класс-расширение>::<метод>) /// </returns> private str getInfoMethodStr(Object _ctrl) { str infoStr; if (this.isSimpleControl(_ctrl)) { if (ctrlChildCustomControl && ctrlChildCustomControl.exists(_ctrl)) { FormContainerControl parentCustomControl = ctrlChildCustomControl.lookup(_ctrl); return this.getInfoMethodStr(parentCustomControl); } if (_ctrl.dataMethod()) { infoStr = _ctrl.dataMethod() + "()"; } } return infoStr; } /// <summary> /// Переопределенный метод контекстного меню, который формирует само контекстное меню /// </summary> /// <param name = "_ctrl"> /// Контрол, к которому формируется контекстное меню /// </param> /// <returns> /// Возвращается перечень пунктов меню (исключая стандартных), которые добавлены в контекстное меню. Перечень /// возвращается в виде строки JSON /// </returns> public str getContextMenuOptions(Object _ctrl) { ContextMenu menu; List menuOptions; str infoDataSourceStr, infoFieldStr, infoMethodStr; str sourceMenu, ret; // Если разработчик уже добавлял пункты контекстного меню, то их перечень необходимо восстановить if (ctrlContextMenu && ctrlContextMenu.exists(_ctrl)) { sourceMenu = ctrlContextMenu.lookup(_ctrl); if (sourceMenu) { // Восстановление ранее добавленных пунктов меню menu = FormJsonSerializer::deserializeObject(classNum(ContextMenu), sourceMenu); sourceMenu = subStr(sourceMenu, strFind(sourceMenu, '[', 1, strLen(sourceMenu)), strLen(sourceMenu)); int endList = strFind(sourceMenu, ']', strLen(sourceMenu), -strLen(sourceMenu)); sourceMenu = subStr(sourceMenu, 1, endList); if (sourceMenu) { menuOptions = FormJsonSerializer::deserializeCollection(classNum(List), sourceMenu, Types::Class, classStr(ContextMenuOption)); } } } // Если пункты меню не добавлялись разработчиком свех стандартных, то перечень пунктов меню инициализируется заново if (!menu || !menuOptions) { menu = new ContextMenu(); menuOptions = new List(Types::Class); } infoDataSourceStr = this.getInfoDataSourceStr(_ctrl); infoFieldStr = this.getInfoFieldStr(_ctrl); infoMethodStr = this.getInfoMethodStr(_ctrl); // Если ссылка на датасорс не заполнена в контроле - то данный пункт меню не выводится. Актуально для // display / edit-методов, которые определены на форме if (infoDataSourceStr) { ContextMenuOption option = ContextMenuOption::Create(strFmt("%1: %2", "@ElectronicReporting:DataSource", infoDataSourceStr), cInfoDataSource); menuOptions.addEnd(option); } // Если ссылка на поле не заполнена в контроле - то данный пункт меню не выводится. Актуально для // display / edit-методов if (infoFieldStr) { ContextMenuOption option = ContextMenuOption::Create(strFmt("%1: %2", "@ElectronicReporting:Field", infoFieldStr), cInfoFieldMethod); menuOptions.addEnd(option); } // Если ссылка на метод не заполнена в контроле - то данный пункт меню не выводится. Актуально для // контролов, привязанных к полям if (infoMethodStr) { ContextMenuOption option = ContextMenuOption::Create(strFmt("%1: %2", "@ElectronicReporting:Method", infoMethodStr), cInfoFieldMethod); menuOptions.addEnd(option); } menu.ContextMenuOptions(menuOptions); ret = menu.Serialize(); return ret; } /// <summary> /// Проверка контрола на его принадлежность к пользовательским контролам /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <param name = "_isStdCtrl"> /// Аргументы, через которые передается информация (Истина / Ложь). По умолчанию предполагается, что контрол /// не является пользовательским, поэтому в обработчике необходимо явно вызвать метод cancel(), если передаваемый /// контрол является пользовательским /// </param> delegate void checkIsCustomControl(Object _ctrl, FormRunServiceArgs _isStdCtrl) { } /// <summary> /// Обработчик делегата checkIsCustomControl для контрола DimensionEntryControl /// </summary> [SubscribesTo(classStr(FormControlShowDevInfo), delegateStr(FormControlShowDevInfo, checkIsCustomControl))] public static void checkIsDimensionEntryControl(Object _ctrl, FormRunServiceArgs _isStdCtrl) { if (!(_ctrl is DimensionEntryControl)) { return; } _isStdCtrl.cancel(); } /// <summary> /// Получение информации о названии датасорса, указанном в пользовательском контроле. Название датасорса необходимо /// указать в классе FormPropertySet, добавив свойство (FormProperty) cPropertyDataSource, в котором и будет передано название /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <param name = "_propertySet"> /// Перечень свойств, через который будет передано название датасорса /// </param> delegate void getDataSourceCustomControl(Object _ctrl, FormPropertySet _propertySet) { } /// <summary> /// Обработчик делегата getDataSourceCustomControl для контрола DimensionEntryControl /// </summary> [SubscribesTo(classStr(FormControlShowDevInfo), delegateStr(FormControlShowDevInfo, getDataSourceCustomControl))] public static void getDataSourceDimensionEntryControl(Object _ctrl, FormPropertySet _propertySet) { if (!(_ctrl is DimensionEntryControl)) { return; } DimensionEntryControl dimCtrl = _ctrl; DimensionEntryControlBuild dimCtrlBuild = dimCtrl.build(); _propertySet.addProperty(FormControlShowDevInfo::cPropertyDataSource, Types::String, dimCtrlBuild.parmDataSourceName()); } /// <summary> /// Получение информации о названии поля, указанного в пользовательском контроле. Название поля необходимо /// указать в классе FormPropertySet, добавив свойство (FormProperty) cPropertyDataFieldName, в котором и будет передано название /// </summary> /// <param name = "_ctrl"> /// Анализируемый контрол /// </param> /// <param name = "_propertySet"> /// Перечень свойств, через который будет передано название поля /// </param> delegate void getFieldNameCustomControl(Object _ctrl, FormPropertySet _propertySet) { } /// <summary> /// Обработчик делегата getFieldNameCustomControl для контрола DimensionEntryControl /// </summary> [SubscribesTo(classStr(FormControlShowDevInfo), delegateStr(FormControlShowDevInfo, getFieldNameCustomControl))] public static void getFieldNameDimensionEntryControl(Object _ctrl, FormPropertySet _propertySet) { if (!(_ctrl is DimensionEntryControl)) { return; } DimensionEntryControl dimCtrl = _ctrl; DimensionEntryControlBuild dimCtrlBuild = dimCtrl.build(); _propertySet.addProperty(FormControlShowDevInfo::cPropertyDataFieldName, Types::String, dimCtrlBuild.parmValueSetReferenceField()); } } Прикрепляю проект в Visual Studio, axpp-файл проекта и axmodel-файл модели (файл модели удобен для быстрой установки)
__________________
Возможно сделать все. Вопрос времени Последний раз редактировалось sukhanchik; 24.11.2020 в 00:00. |
|
|
За это сообщение автора поблагодарили: trud (10), raz (10), S.Kuskov (10). |