Рефакторинг десктоп функционала для поддержки в веб-доступе

21 0

Итак, вы разработчик. Ваша задача - заставить работать в веб-доступе DIRECTUM то, что уже функционирует в десктоп-клиенте. Воспользуйтесь планом ниже:

Шаг 0. Успокойтесь и настройтесь на разработку.

Шаг 1. Проанализируйте существующую прикладную разработку. 

В первую очередь при рефакторинге ПЧ под веб необходимо определить список мест, в которых будут происходить ошибки. К ним относится:

  • Показ диалогов и сообщений;
  • Показ форм объектов системы (текущей или нет).

Шаг 2. Изолировать отображение окон, сгруппировать общие вычисления в функции или методы.

Шаг 3. Отображение окон или форм объектов реализовать в веб-модуле. И организовать правильный вызов прикладного кода. Для этого используйте следующие возможности:

  • Для запроса данных и отображения информации использовать объект IDialog.
  • Методы объектов. Сложная логика получения данных останется в методе, на уровне веб-модуля потребуется лишь отображение данных.
  • Если методы не подходят по каким-то причинам (например, конкретного объекта нет - смело используйте сценарии. Вызов оных из веб-модуля является простым и надежным способом. В сценарии используйте те же функции, что в аналогичном функционале десктоп-клиента. 

Шаг 4. Измените разработку прикладной части и добавьте обработчики в веб-модули, ориентируясь на результаты третьего шага. 

Рассмотрим применение этих шагов на практике.

Диалоги

Пример использования диалога при прекращении Поручения из карточки справочника

При прекращении поручения необходимо запросить причину прекращения. Для этого реализован прикладной диалог, который вызывается на действии.

 Dialog = Dialogs.AssigmentAbort.CreateNew
 if not IsNOMADRuntimeContext() and not IsWebRuntimeContext()  
     RecordID = Object.SYSREQ_ID     
     Dialog.AbortType = 'E'
     Dialog.ShortString = RecordID
     RecordOpened = Object.RecordOpened 
   else
     RecordOpened = FALSE
   endif
   ShowDialog(Dialog)
   Reason = Dialog.Requisites('LongString').Value
   RecordID = Dialog.Requisites('ShortString').Value   
   ResultDialog = Dialog.Result
   if ResultDialog <> mrCancel
     // Прекратить задачи по текущему поручению и всем его подчиненным, послать уведомления о прекращении работ, 
     // изменить статус поручений и снять их с контроля
     if IsWebRuntimeContext()
       Object.Params.Add('AssignmentIsAborted'; TRUE)
     endif                                     
     EndWorkForAssignment(RecordID; Reason; 'E')    
     if not IsNOMADRuntimeContext() and not IsWebRuntimeContext()
       if RecordOpened 
         Actions = Object.Form.Actions
         // Установить доступность кнопок напомнить о сроке, скорректировать, прекратить, принять, на доработку, запросить отчет
         ActionNameArray = "NotifyAboutDeadline|CorrectAssignment|StopWork|Assept|Revision|RequestState"
         foreach ActionName in CSubString(ActionNameArray; '|')
           Action = Actions.FindAction(ActionName)
           if not VarIsNull(Action)
             Action.Enabled = FALSE
           endif
         endforeach  
       endif
     endif 
 endif

Логика прекращения поручения достаточно сложная, поэтому дублирование ее еще и в веб-модуле нецелесообразно.

Функция в веб показывает диалог, а затем запускает само действие в ПЧ с уже заполненными параметрами. Запуск действия (а не сценария) позволяет также обновить карточку Поручения автоматически.

 // Прервать исполнение поручения
 RecordOffice.abortAssignment = function () {
 var dialogName = 'AssigmentAbort';
     var actionName = 'StopWork';
     WA.FC.dialogs.getObjectByName(dialogName, MODEL.FULL).done(function (dialog) {
       dialog.show();
       dialog.setTitle(L('ASSIGNMENT_DIALOG_ABORT'));
       dialog.Init.done(function () {
         dialog.form.requisites['AbortType'].setValue('1');
         dialog.form.requisites['ShortString'].setValue(WA.CR.ID);
       });
      dialog.bind(WA.CMP.dialogs.FormDialog.EVENT.AFTER_ACTION, function (dialogActionName, result) {
         // Нестандартная кнопка
         if (dialogActionName === 'OK') {
           WA.SRV.call('/Action.asmx/Execute', {
             Link: (new WA.Link()).toLinkModel(), ActionName: actionName,
             SerializedForm: dialog.form.serialize({
               onlyValue: true,
               returnRowNumber: true,
               withoutTables: true
             }),
             CommandNumber: 0, CommandType: 0, CommandResult: {}
           })
           .done(function (response) {
             var responseAsObject = $.parseJSON(response);
             // Выполнить комманды
             var cmds = responseAsObject.Commands;
             WA.CR._performCommands(cmds);
            // Применить изменения к форме
            var changes = responseAsObject.Changes;
            WA.CR.form.applyServerSideChanges(changes);
             _disableAssignmentButtons();
             WA.CR.inlineHint.showInfo(L('ASSIGNMENT_TASK_WAS_ABORTED'));
             FreeRecord(WA.CR.ReferenceCode, WA.CR.ID);
           });
         }
      });
   });
 };

Пример использования событий диалогов с минимальным дублированием функционала в веб-модуле

Часто, чтобы пользователю было удобнее, требуется предзаполнить реквизиты диалога, а после успешного запроса всех параметров выполнить какую-либо логику.

Удобнее всего будет реализовать всю логику на стороне ПЧ, а в веб-доступе только отобразить диалог.

Рассмотрим пример: в Исходящих РКК есть кнопка "Контролировать возврат", оформлена в действии "Контроль возврата" (CheckReturn), для запроса данных и формирования задачи реализован прикладной диалог "Контроль возврата документа" (DISICheckReturn).

Действие построено следующим образом:

  1. Получить диалог.
  2. Заполнить реквизит диалога "РКК" (Int).
  3. Отобразить диалог пользователю.
  4. Вывести сообщение об успешности отправки задачи.
  Dialog = Dialogs.DISICheckReturn.CreateNew()
  Dialog.Int = Object.SYSREQ_ID
  ShowDialog(Dialog)
  if Dialog.Result = mrOk
    // Задача на контроль возврата успешно отправлена.
    ShowMessage(LoadString('DIRE6F944C2_21AF_4002_8F4A_C2C0498F8C52'; 'RM'))
  endif

Как можно заметить, вся основная логика расположена в самом диалоге. Разберём по порядку.

1. Предзаполнение реквизитов диалога значениями по-умолчанию можно вынести на событие "Создание" диалога.

  // Заполнить поля диалога
  Object.Date = ServiceFactory.GetRelativeDate(Today(); 21; dotDays)
  Object.User = Tasks.CurrentUser.Code

2. Чаще требуется предзаполнять реквизиты в зависимости от контекста. Выше, мы указали ИД РКК в реквизите диалога Int. На событии Вычисление реквизита Int вычислим пользователя от РКК

  // Заполнить поля диалога
  Employee = GetRequisiteValueAsString('РКК'; ''; 'Employee';;; Object.Int)
  // Найти пользователя по работнику
  if Employee <<>> ''
    Object.User = GetRequisiteValueAsString('РАБ'; Employee; 'Пользователь')
  endif

3. На событии Закрытие Возможность проверим валидность данных

  if DateDiff('D'; Today(); Object.Date) < 0
    // Указанная дата меньше текущей.
    Raise(CreateException('EDIRInvalidDate'; LoadString('DIRE3CB79BB_B019_4B0F_8F2F_86B0BE78E90A'; 'MM'); ecWarning))  
  endif

4. И в завершение, на событии Закрытие запустим серверное событие, которое сформирует задачу по контролю возврата.

  // Стартовать задачу на котроль возврата
  if Object.Result = mrOk
    RRCID = Object.Int
    DocIDs = Object.LongString
    if Assigned(RRCID) or Assigned(DocIDs)
      // Сформировать список общих параметров
      ParamStringList : IStringList = CreateStringList()
      ParamStringList.Delimiter = CONST_ELEMENT_DELIMITER
      ParamStringList.Add('UserName' & CONST_VALUE_DELIMITER & CurrentUserName())
      ParamStringList.Add('CheckReturnUserID' & CONST_VALUE_DELIMITER & Object.Requisites("User").ValueID)
      ParamStringList.Add('CheckReturnDeadline' & CONST_VALUE_DELIMITER & Object.Requisites("Date").AsString) 
      ParamStringList.Add('AccompanyDocsIDs' & CONST_VALUE_DELIMITER & Object.Requisites("LongString2").AsString)
          
      // Запустить серверное событие для отправки задачи на контроль возврата зарегистрированного документа
      ServerEventScript = ServerEvents.GetObjectByName('DISICheckReturn') 
      Params = ServerEventScript.Params 
      Params.ValueByName('TaskID').Value = Object.Int3 
      Params.ValueByName('RRCID').Value = RRCID 
      Params.ValueByName('DocIDs').Value = DocIDs 
      Params.ValueByName('StringParamList').Value = ParamStringList.DelimitedText 
      ServerEventScript.Start 
    endif
   endif

В веб-модуле создаём функцию, которая будет отображать диалог по кнопке в РКК.

/**
* Контролировать возврат документа
* @method RecordOffice.checkReturn
**/
RecordOffice.checkReturn = function () {
  // Заполнить реквизиты диалога и отправить задачу на контроль
  WA.SRV.call("/RecordOffice.asmx/GetCheckReturnDialog", { ID: WA.CR.ID }).done(function (res) {
    WA.FC.dialogs.getDialogByInstanceID(res).done(function (dialog) {
      dialog.show();
      dialog.bind(WA.CMP.dialogs.FormDialog.EVENT.AFTER_ACTION, function (dialogActionName, result) {
        if (dialogActionName === DIALOG_ACTION.OK) {
          ShowInfo(L("CHECK_RETURN_TASK_SEND"));
        };
      });
    });
  });
}

Для заполнения реквизита диалога "РКК" создаём веб-метод

  ''' <summary>
  ''' Подготавливает диалог для контроля возврата по документу. 
  ''' </summary>
  ''' <param name="ID">ИД РКК</param>
  ''' <returns>ИД диалога</returns>
  <WebMethod>
  Public Function GetCheckReturnDialog(ByVal ID As Integer) As WebServiceResponse(Of String)
    Dim Dialog As API.Dialog = Nothing
    Dialog = WebSession.Context.Dialogs.GetDialogByName(Constants.CHECK_RETURN_DIALOG_NAME)
    Dialog.Requisites("Int").Value = ID
    Return WebServiceResponse(Of String).OK(Dialog.InstanceID)
  End Function

Как видим, в веб-модуле потребовалось минимальное дублирование функционала, вся основная логика подхватывается с событий диалога автоматически.

Необходимо учитывать это при разработке и максимально переносить вычисления на события диалога.

Функция ShowDialog()

Функция ShowDialog() реализована для запроса данных для формирования отчета.

Работает с iDialog следующим образом: для десктопа отображает диалог, для веб-доступа заполняет реквизиты диалога одноименными параметрами родительского объекта (Object.Params).

Пример вызова в ПЧ:

   // Создать и показать диалог запроса параметров
   Dialog = Dialogs.AssignmentExecutionDeadlineControl.CreateNew  
   if not VarIsNull(Object.Params.FindItem('Cut'))
     if Object.Params.ValueByName('Cut') == 'Meeting'
       Cut = MeetingStr
     else 
       if Object.Params.ValueByName('Cut') == 'Claim'
         Cut = ClaimStr
       else
         if Object.Params.ValueByName('Cut') == 'RRC'
           Cut = RRCStr
         endif 
       endif 
     endif   
     Dialog.AssignmentCut = Cut
   endif
   ShowDialog(Dialog)
   if Dialog.Result = mrOk or IsWebRuntimeContext()
 …

В вебе требуется минимальная доработка: вставить в xml-ку строку с указанием, что параметры отчета будут браться из диалога, и имени диалога:

 <Report name="AssignmentExecutionDeadlineControl" getparams="fromdialog" dialogname="AssignmentExecutionDeadlineControl" />

Методы

Сценарии используются в случае, если нет возможности использовать метод: работа идет с несколькими объектами, или этот объект не имеет методов. Например, для изменения документа из карточки задания используется следующий фрагмент js кода:

  // Вызвать сценарий для смены ВЭД и ТКЭД документа
  WA.FC.scripts('WADISITransformDoc').execute({ DocID: docID, DocKind: docKind, DocType: docType, Text: text, InvoiceType: invoiceType })
    .success(function (Doc) {
      // Открыть карточку документа
      var docUrl = docLink.toURL();
      docUrl.setParam('mode', 'edit')
      var docWin = docUrl.open();
      // Ждать, пока не закроют
      var timer = setInterval(function () {
        if (docWin.closed) {
          clearInterval(timer);
          if (dialog)
            dialog.close();
        };
      }, 1000);
    });

Вместо сценария предпочтительно использовать методы объектов. Потому что это проще и менее трудоемко. Как минимум, не потребуется выдавать права на новые сценарии.

Типовые случаи применения методов:

1. Если в действии происходят сложные вычисления, результатом которых являются открытие объектов или вывод сообщения.

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

Пример открытия задачи

Используйте для простоты разработки общие функции для открытия окон/объектов

  • showDialogAndExecuteAction: отобразить диалог, после чего выполнить действие.
  • startTaskBySR: отправить задачу по типовому маршруту.
  • openTaskInNewTab: открыть задачу на новой вкладке.

Например, 

  /**
  * Функция для отправки договорного документа на согласование 
  * @method ContractsManagement.sendContractForApproval
  */
  ContractsManagement.sendContractForApproval = function () {
    WA.CR.executeEntityMethod("OnExecute_Задача")
      .done(function (TaskIDStdRoute) {
        if (TaskIDStdRoute[0]) {
          BaseWebAccess.openTaskInNewTab(TaskIDStdRoute[0]);
        }
        else {
          SendAsAttachment({
            id: WA.CR.ID,
            kind: WA.CR.getKind()
          }, TaskIDStdRoute[1], '');
        };
      })
    .fail(function (message) {
      // Если константа CMAgreementStandardRoute не означена, то выводить исключение 
      WA.CR.inlineHint.showError(message);
    });
  };

Пример метода для создания новой записи с открытием записи

В самом методе нужно реализовать создание записи. А вот открытие карточки записи должно отличаться для веб и десктоп-клиента: в контексте веб-доступа надо вернуть созданную запись как результат, для того, чтобы потом отобразить запись на уровне веб-модуля:

  // Запретить изменять не ключевым участникам
  if not MMCurrentUserIsMeetingKeyFigure(Object; TRUE) and Object.Environment.IndexOfName('From_Agent') = -1
    // Неключевым участникам запрещено изменять совещание. Ключевыми участниками являются инициатор, секретарь, председатель и их замещающие.
    Message = LoadString('DIR52F15441_539B_41B9_8AFB_BCBC9FDFD475'; 'MM')
    Raise(CreateException('EDIRInvalidUserAction'; Message; ecWarning))
  endif
  MeetingRef = References.СВЩ.GetComponent 
  MeetingRef.ViewName = 'Главное'  
  AddWhere = MeetingRef.AddWhere("1 = 2")
  MeetingRef.Open
  MeetingRef.Append
  MeetingRef.Requisites("MTemplate").Value = Object.SYSREQ_CODE
  if not IsWebRuntimeContext()
    MeetingRef.Form.ShowModal
    MeetingRef.Close
    MeetingRef.DelWhere(AddWhere)
  else
    MeetingRef.DelWhere(AddWhere)
    Result = MeetingRef
    MeetingRef = nil
  endif

В веб-модуле вычисления будут выглядеть так:

  /**
  * Функция для создания совещания по серии
  * @method Meetings.CreateSeriaMeeting
  */
  Meetings.CreateSeriaMeeting = function () {
    WA.CR.executeEntityMethod("OnExecute_MeetingCreate")
      .done(function (res) {
        var rrctLink = new WA.Link(res.id, OBJECT_TYPE.REFERENCE_RECORD, 'СВЩ');
        var rrcUrl = rrctLink.toURL();
        rrcUrl.setParam("refview", "Главное");
        rrcWin = rrcUrl.open();
      });
  };

Пример вызова диалога в методе

В случае, если перед вычислениями нужно запросить данные у пользователя, используем комбинированный вариант: диалог+метод. Вычисления в веб-модуле будут выглядеть так:

  /**
  * Отменить совещение
  * @method Meetings.cancelMeeting 
  */
  Meetings.cancelMeeting = function () {
    WA.FC.dialogs.getObjectByName('MMMeetingCancel', MODEL.FULL).done(function (dialog) {
      dialog.show();
      dialog.bind(WA.CMP.dialogs.FormDialog.EVENT.AFTER_ACTION, function (dialogActionName, result) {
        if (dialogActionName === DIALOG_ACTION.OK) {
          WA.CR.executeEntityMethod("OnExecute_MeetingCancel", dialog.form.requisites['LongString'].getValue());
        }
      });
    });
  };

Для того, чтобы это заработало, нужно:

1. Реализовать диалог для запроса данных у пользователя. Возможно, к моменту разработки он уже будет.

2. В метод добавить параметр CancelReason. И, разумеется, сделать метод видимым.

3. В текст метода добавить вычисления.

  // Запретить изменять не ключевым участникам
  if not MMCurrentUserIsMeetingKeyFigure(Object)
    // Неключевым участникам запрещено изменять совещание. Ключевыми участниками являются инициатор, секретарь, председатель и их замещающие.
    Message = LoadString('DIR9379F646_5AF4_489F_8A05_D43D69C53CA9'; 'MM')
    Raise(CreateException('EDIRInvalidUserAction'; Message; ecWarning))
  endif
  Object.Environment.SetVar('CANCEL_RECORD'; TRUE) 
  Reason = MeetingCancelInitiate(Object;; CancelReason) 
  Object.СостСовещ = 'Прекращено'
  if Reason <<>> ''
    Object.Room = ''
    Object.Дополнение = ''
  endif
  Object.Events.DisableAll
  Object.Rules.DisableAll
  Object.Save
  Object.Events.EnableAll
  Object.Rules.EnableAll
  
  // Установить доступность кнопок для задач 
  Object.OnExecute_SetButtonsVisibility
  
  if Object.Environment.IndexOfName('CANCEL_RECORD') > -1 
    Object.Environment.PopVar('CANCEL_RECORD')
  endif  

Заключение

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

Команда стандартного веб-доступа всеми силами стараются упростить разработку веб-модулей и обеспечить поддержку прикладного кода в веб-клиенте. Дело это длительное и трудоемкое.

Если у вас есть пожелания к модификации веб-доступа или веб-модулей, оформите обращение в DIRECTUM. 

Пока комментариев нет.

Авторизуйтесь, чтобы написать комментарий