Элементы быстрой разработки при построении систем на FileMaker

Автор: Андрей Волков

Доклад на ежегодной конференции разработчиков на платформе файлмейкер FileMaker DevCon Russia 2017

Приветствую, коллеги.

Мое выступление посвящено средствам быстрой разработки на платформе FileMaker. Это тот случай, когда  раскрыть содержание проблемы проще “на пальцах”, то есть на конкретных жизненных примерах. И в качестве демонстрационного примера я выбрал “Справочники”.

Справочники используются практически в каждой информационной системе, базе данных. Справочник — это обычно небольшая таблица, которая постоянно пополняется новыми данными, и к которой пользователи регулярно обращаются в процессе работы с базой данных.

Пример: таблица “Пользователи/Персонал” (users). В нее заносятся все пользователи системы, или вообще все работники организации. Стандартное использование справочника — выбор какого-либо работника ответственным в проекте. Или выбор нескольких работников в качестве проектной группы. Или выбор ответственного в задаче. Или выбор куратора какой-то задачи.

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

Каждый раз, когда мы осуществляем операцию выбора значения из таблицы, мы, можно сказать, фактически “работаем со справочником”. Файлмейкер предлагает нам для выбора значения стандартные средства: всплывающие меню и попап меню. И когда нам этого достаточно, мы этими средствами и пользуемся.

Но часто нам бывает необходимо не просто выбрать значение, но и иметь возможность добавить новое, исправить-отредактировать существующее, удалить ненужное, произвести поиск, если количество записей в таблице слишком велико. Бывает удобно выдать пользователю не все записи таблицы целиком, а некий ограниченный набор. Ну, и наконец, бывает необходимо дать пользователю произвести множественный выбор или мультиселект.

Вот эти все операции в совокупности и можно заменить выражением “работа со справочниками”.

Pic1


Кто работал с 1С, хорошо представляет себе как в этой системе организована работа со справочниками. В принципе нечто подобное хочется видеть и в FileMaker.

Давайте формализуем наши требования:

Нам нужно при нажатии кнопки на макете вызвать скрипт, который отобразит диалоговое окно, в котором мы увидим полный либо ограниченный список значений одной таблицы. В этом окне мы имеем возможность выбрать любую запись (или записи в случае мультиселекта), при этом диалог будет закрыт, а скрипт возвратит идентификатор выбранной записи.

Справочник - список

Также в этом окне пользователь должен иметь возможность сортировать данные по столбцам,  производить поиск, и если у него есть достаточные права, — редактировать любую запись, добавить новую или удалить ненужную строку.

Справочник - элемент

Для пользователя работать с таким диалоговым окном будет весьма удобно. А что разработчик? Разработчик, обладающий достаточным опытом, конечно же, сможет разработать такой интерфейс. Но, как мы понимаем, механизм здесь нетривиальный. И, получается, для каждой используемой в справочных целях таблицы нам придется все это копировать, множить скрипты, вести отладку…. Не слишком ли сложно?

Вот здесь мы и приближаемся к теме средств быстрой разработки. На форуме не часто заходит речь об этом, хотя, я уверен, каждый разработчик имеет в наработках некие универсальные схемы, решения, скрипты, кастом-функции, модули…  Мы как-то неохотно делимся этим, подразумевая, что эти все наши ноу-хау являются коммерческой тайной, которые дают нам некое конкурентное преимущество. Или быть может, мы стесняемся наших разработок?

Сегодня я расскажу о том, как создать модуль “Справочники”, который позволит вовлекать в “справочную систему” каждую новую таблицу буквально за пять минут.

В чем секрет этого модуля? Он разработан таким образом, что все используемые в нем скрипты и элементы управления универсальны. Когда мы создаем “справочник” для некоей новой таблицы, мы не множим скрипты и не перенастраиваем кнопки. Нам достаточно всего лишь сделать дубликаты двух или трех (если используется мультиселект) макетов, в свойствах макетов указать новое имя и таблицу-источник,  и подставить нужные поля.

Спр_изм_элементы

Рассмотрим, как устроены макеты и какие сценарии используются

Справочник_кнопки

 

 

На иллюстрации отмечены элементы управления — кнопки. Если каждая кнопка — это отдельный сценарий, то нам требуется 12 сценариев, включая стартовый скрипт.

  • Запуск диалогового окна.
  • Кнопка “Добавить запись”
  • Кнопка “Удалить запись”
  • Поиск в справочнике (отрабатывает по триггеру).
  • Очистка поля ввода
  • Сортировка данных по столбцам
  • Кнопка “Редактировать запись”
  • Кнопка “выбрать”
  • Кнопка “Отменить”
  • Кнопка “Сохранить в диалоге “Редактирование записи”
  • Кнопка “Отменить в диалоге “Редактирование записей”
  • Триггер OnCommit в диалоге “Редактирование записей”

Сколько бы мы ни делали макетов для целей  нашей информационной системы, все рассмотренные кнопки и сценарии не изменяются и используются “как есть”.

Далее мы рассмотрим каждый сценарий в отдельности и отметим способы, которыми мы достигаем универсальности.

Первый сценарий открывает диалоговое окно, выравнивает диалог по центру, переводит на нужный макет и открывает бесконечный цикл, выход из которого возможен только при нажатии кнопок “Выбрать” или “Отменить”

Сценарий 1. Open dialog

# этот скрипт открывает окно диалога
# параметр - наименование лэйаута
Set Variable [ $param; Value:Get(ScriptParameter) ]
If [ IsEmpty($param) ]
   Close File [ Current File ]
End If
Go to Layout [ $param ]

# настройки окна - выравнивание по центру и т.д.
Show/Hide Toolbars [ Hide ]
Select Window [ Current Window ]
Move/Resize Window [ Current Window; Top: CH; Left: CW ]

# заголовок окна
Set Variable [ $title; Value:GetLayoutObjectAttribute ( "title"; "content" ) ]
Go to Object [ Object Name: "QUICK_FIND" ]

# если задано условие поиска, то выполняется поиск
Set Variable [ $filter; Value:Evaluate(Get(LayoutTableName) & "::tmp_self_id") ]
If [ not IsEmpty ($filter) ]
   Perform Quick Find [ "*" ]
End If

# делаем окно модальным
Allow User Abort [ Off ]
Loop
   Exit Loop If [ not IsEmpty($RESULT) ]
   Pause/Resume Script [ Indefinitely ]
End Loop

# окно можно закрыть, я его просто делаю невидимым
Allow User Abort [ On ]
Adjust Window [ Hide ]

# выход из скрипта. возвращает ид выбранной строки или ноль
Exit Script [ Result: $RESULT ]

Название нужного нам макета мы передадим в параметре, а в команде Go to Layout используем опцию Layout Name by Calculation. Скрипт стал универсальным.Он способен выполнять еще одну функцию — отбор строк для первоначального отображения, но мы ее отметим позже. Сейчас мы видим, что сценарий выполняет для каждого справочника одни и те же действия, за исключением одной строки, в которой производится переход на искомый макет.

Кнопка 6. Сортировка данных по столбцам.

Set Variable [ $field; Value:Get(ScriptParameter) ]
Go to Object [ Object Name: $field ]
Sort Records by Field [ Ascending ]

Скрипт отрабатывает клик по кнопке в заголовке столбца. Используется стандартный скрипт $_SORT, которому в качестве параметра передается имя объекта. Объектами в нашем случае являются поля в столбцах ID и Наименование. Имя объекта задается один раз и при копировании макетов не изменяется. Ну, а сам сценарий — элементарный: он перемещает нас к нужному объекту-полю, а затем выполняется команда Sort Records by Field.

Можно использовать более сложный сценарий, который позволит осуществлять сортировку каждого столбца в двух направлениях. В этом случае объекты-поля должны именоваться как sort1 и sort2, а кнопки в заголовках столбцов должны вызывать скрипт с параметрами соответственно 1 и 2. Но тогда требуется размещать на макете еще одно поле ID , которое должно иметь объектное имя id. Далее будет понятно, зачем.

Set Variable [ $i; Value:Get(ScriptParameter) ]
Set Variable [ $Layout; Value:GetLayoutID ]
Set Variable [ $state; Value:$$STATE[$layout] ]
Set Variable [ $state; Value:If($i = Abs ( $state );$state * -1;$i) ]
Set Variable [ $$STATE[$layout]; Value:$state ]

Set Error Capture [ On ]
# Sort ascending
Freeze Window
Go to Object [ Object Name: "sort" & $i ]
If [ $state > 0 ]
   Sort Records by Field [ Ascending ]
Else If [ $state <0 ]
   Sort Records by Field [ Descending ]

   # Unsort records
   Else If [ IsEmpty($state) ]
   Unsort Records
End If

Commit Records/Requests
Go to Record/Request/Page [ First ]
Exit Script [ ]

Использование JSON функций в версиях 16 и следующих позволит усовершенствовать последний вариант: в параметр скрипта можно передавать любое имя объекта, не прибегая к именам типа “sort1”.

Кнопка 3. Удаление элемента справочника.

Set Variable [ $allow; Value:Evaluate(Get(LayoutTableName) & "::aDelete") ]
If [ not $allow ]
   Exit Script [ ]
End If
Delete Record/Request

Примечание. Скрипт адаптирован к версиям FileMaker, в которых невозможно скрытие записи. Для новых версий первая часть скрипта не актуальна. Можно по такому же условию вычислить права пользователя на удаление записи и скрыть кнопку для всех, у кого таких прав нет.

Чтобы вычислить права пользователя, в каждой таблице-справочнике должно существовать калькулируемое поле с условным наименованием aDelete, которое возвращает булево значение (1 если разрешено удалять записи, 0 если запрещено). Вычисление значения производится с помощью функций Evaluate() и Get(LayoutTableName)

Если поле с именем aDelete в таблице отсутствует физически, то удаление записи невозможно.

 

Кнопка 7. Редактирование записи.

Первая часть скрипта не нуждается в комментариях.

Set Variable [ $allow; Value:Evaluate(Get(LayoutTableName) & "::aEdit") ]
If [ not $allow ]
   Exit Script [ ]
End If
Set Variable [ $layout; Value:Get(LayoutName) ]
Go to Layout [ Substitute($layout; "list"; "detail") ]
Set Variable [ $$RESULT ]

Строки 5 и 6 показывают, как можно использовать стандартные наименования макетов. Макет-список и макет для отображения элемента отличаются в названии правой частью. Список дописывается с постфиксом  _list (например, spr_table_list), элемент дописывается как _detail (например, spr_table_detail). Благодаря этому получаем безошибочное перенаправление на нужный лэйаут.

Кнопка 2. Добавление элемента.

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

Триггер 4. Поиск в справочнике.

В этом скрипте используется сразу несколько специальных полей. Глобальное поле QUICK_FIND — это собственно поле ввода, в котором мы указываем критерии поиска. В принципе его можно заменить любым другим, скрипт не обращается напрямую к полю, но считывает параметр из активного объекта.

Скрипт (см. листинг ниже) ведет себя двояко. Если в поле «Поиск» введено некое значение, то выполняется поиск записей, удовлетворяющих критериям поиска. Если поле было очищено, то выполняется поиск всех записей, содержащих символ «*» (звездочка).

# выполняет поиск по специальному полю либо отображаются все записи
#триггер - обрабатывается выход из поля поиска
Set Variable [ $string; Value:Get(ActiveFieldContents) ]
Set Error Capture [ On ]
If [ not IsEmpty($string) ]
   Enter Find Mode [ ]
   Go to Object [ Object Name: "find" ]
   Set Field [ $string ]
   Perform Find [ ]
   Exit Script [ ]
Else
   Perform Quick Find [ "*" ]
   Go to Object [ Object Name: "QUICK_FIND" ]
End If
Exit Script [ ]

Поиск выполняется в поле, которому присвоено объектное имя «find». И это не поле Наименование (Name), как можно было бы предположить изначально. Объект «find» — это специальное поле. Оно вынесено за пределы макета.

cQuickFind

Калькулируемое поле cQuickFind имеет формулу:

Let(~searchstring = id & " " & Name & " " & short_name & " *";
   Case(
      IsEmpty(TMP_SELF_ID); ~searchstring;
      FilterValues(TMP_SELF_ID; id); ~searchstring;
   )
)

Как видно из формулы, оно обращается к полю TMP_SELF_ID и проверяет его значение. Зачем это нужно? Глобальное поле TMP_SELF_ID необходимо для того, чтобы иметь возможность ограничить набор записей в справочнике. Если оставить это поле пустым (это будет состояние по умолчанию), то это означает, что пользователь будет иметь в справочнике доступ ко всем записям. Если же с помощью ExecuteSQL произвести выборку id интересующих записей и поместить их в поле TMP_SELF_ID, то справочник будет работать следующим образом. Все записи, которые попали в выборку, формируют в поле cQuickFind непустую строку поиска, состоящую из полей id, name и символа «звездочка». У всех остальных записей поле cQuickFind остается  пустым.

 

Теперь сценарий нам понятен: для поиска записей мы будем использовать команду Perform Find и искать будем только в поле cQuickFind (в качестве критерия можно указывать как наименование, так и  ID записи). А для отображения “всех записей” будем использовать команду QuickFind (можно делать поиск по «звездочке», но Perform Quick Find работает быстрее). Макет настроен так, что только одно поле участвует в быстром поиске. Ищем символ “звездочка”, который обязательно присутствует в записях, которые разрешено видеть в справочнике.

Становится понятным и поведение стартового сценария OpenDialog.

Перед запуском диалога поле  TMP_SELF_ID очищается или, если необходимо,  заполняется списком идентификаторов нужных записей. Стартовый скрипт отреагирует на заполнение поля TMP_SELF_ID и произведет первоначальную фильтрацию и отсеет “лишние” значения. Любой последующий поиск и его «сброс» всегда будет выполняться в рамках ограниченного набора записей.

 

 

Редактирование элемента справочника

Регулируется четырьмя скриптами.

Собственно скрипт, который переводит пользователя на специальный макет для редактирования:

Set Variable [ $allow; Value:Evaluate(Get(LayoutTableName) & "::aEdit") ]

If [ not $allow ]

   Exit Script [ ]

End If

Set Variable [ $layout; Value:Get(LayoutName) ]

Go to Layout [ Substitute($layout; "list"; "detail") ]

Set Variable [ $$RESULT ]

Триггер (Layout — OnRecordCommit), который предотвращает сохранение отредактированной записи кликом мыши:

If [ not $$RESULT ]

   Exit Script [ Result: 0 ]

End If

Скрипт, который подтверждает изменение записи:

Set Variable [ $layout; Value:Get(LayoutName) ]

Set Variable [ $$RESULT; Value:1 ]

Go to Layout [ Substitute($layout; "detail"; "list") ]

Скрипт, который отменяет изменение записи:

Set Variable [ $edited; Value:Get(RecordOpenState) = 1 ]

If [ $edited ]

   Show Custom Dialog [ Title: "Подтверждение операции"; Message: "Отменить все изменения?"; Default Button: “Нет”, Commit: “Yes”; Button 2: “Да”, Commit: “No” ]

   If [ Get(LastMessageChoice) = 1 ]

      Exit Script [ ]

   End If

End If

Revert Record/Request [ No dialog ]

Set Variable [ $layout; Value:Get(LayoutName) ]

Go to Layout [ Substitute($layout; "detail"; "list") ]

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

 

Выбор элемента справочника.

Основные кнопки внизу справочника — “Выбрать” и “Закрыть” — выполняют один и тот же сценарий: Close dialog. Сценарий состоит всего лишь из одной строки.

Exit Script [ ]

Секрет этих кнопок — в настройке.

В каждой кнопке задана опция Resume Current Script

ButtonSetup

В каждой кнопке задается параметр скрипта

Let($result = GetLayoutObjectAttribute ( "id"; "content" ); "")
Let($result = 0; "")

ButtonParam

Что делают эти кнопки на самом деле? Они прекращают заданный стартовым скриптом цикл и одновременно устанавливают значение переменной $result в этом стартовом скрипте равным 0 (кнопка Закрыть) или равным идентификатору элемента справочника. Скрипт завершает свое действие с результатом, равным $result. Этот результат мы можем считать функцией Get(ScriptResult) и интерпретировать: результат равен нулю — значит, пользователь отказался от выбора значения из справочника. Если результат больше нуля — это id интересующей нас записи.

И снова мы обошлись без ссылок на конкретные поля и макеты.

Задача решена. Теперь мы можем копировать макеты и создавать удобные справочники с унифицированным интерфейсом и логикой. При этом вовсе не нужно каждый раз настраивать скрипты. Созданные один раз сценарии можно повторно использовать не только в рамках одного решения, но и переносить в другие проекты.

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

  • Использование универсальных кастом-функций, скриптов, модулей.
  • Стандарт на наименование макетов
  • Использование специальных полей (таких как aEdit, aDelete)
  • Стандарт на наименование полей (поля id, name)
  • Использование логических, дизайнерских, гет-функций (Logical / Design / Get  functions).
  • Использование специальных таблиц
  • Использование мета-таблиц
  • Использование SQL

 

В продолжение темы хотелось поделиться примерами использования специальных полей

Ранее были упомянуты поля:

aNew, aEdit, aDelete, QUICK_FIND, TMP_SELF_ID, cQuickFind

Они используются практически в каждой таблице

Всем знакомо CONST.1, на форуме мы обсуждали поле trigger

Наверняка каждый разработчик использует поля, хранящие время-и-акаунт создания записи и время-и-акаунт последнего изменения записи. Такие стандартные поля можно заводить сразу при создании каждой новой таблицы, вместо того, чтобы включать в таблицу постепенно, по необходимости. Но это вопрос дискуссионный…

Несколько раз мне пригодилось поле creation_data c опцией Auto-Enter Calculation, которое запоминает при создании дату, время, пользователя, макет и название скрипта.

Практика показала огромную выгоду использования таких полей как table_ref, parent_ref, parent_id

Table_ref — числовое поле , которое хранит числовой идентификатор таблицы, это константа. Идентификатор таблице разработчик присваивает самостоятельно. Parent_ref — числовое поле, содержащее ссылку (внешний ключ) на поле table_ref в родительской таблице, а parent_id указывает на поле id в той же таблице.

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

 

Например, у нас есть таблица Notes, которая хранит примечания к любой записи в любой сущности. Независимо от того, сколько сущностей мы выделили в проекте, связь любой из них с таблицей Notes будет однотипной:

 

Project::id         =  Notes to project::parent_id
Project::table_ref  =  Notes to project::parent_ref
Contact::id          =  Notes to contact ::parent_id
Contact ::table_ref  =  Notes to contact ::parent_ref
Order::id         =  Notes to order ::parent_id
Order::table_ref  =  Notes to order ::parent_ref

 

Это не единственно возможное использование поля table_ref

Представим, что у нас в системе есть таблица MyTables, в которой мы перечисляем все используемые нами физические таблицы. В этой мета-таблице мы можем указать идентификатор, наименование физической таблицы, ее описание (назначение), наименование на русском языке, русское наименование для справочника, для формы, для списка, идентификатор основного макета, макета-справочника, макета-сортировки.

Через поле table_ref в любой из таблиц базы данных мы получаем ссылку на мета-таблицу MyTables и можем с помощью запросов SQL вычислить нужные нам параметры текущей физической таблицы. Это способно в ряде случаев упростить и ускорить разработку. В нашем примере со справочниками мы могли бы вычислять заголовок справочника вместо того, чтобы менять этот заголовок вручную.

Другим примером мета-таблиц могут служить таблицы Fields, Layouts, Scripts. Их содержимое аналогично: они перечисляют все поля, макеты и скрипты. Заполнять эти таблицы можно автоматически, по ежедневному расписанию, используя дизайн-функции. На настоящий момент возможности практического использования мета-таблиц Fields и Scripts еще остаются неясными. Как минимум, по ним можно наблюдать прогресс создания базы данных, фиксировать, когда что создавалось, контролировать правильность оформления скриптов  (комментирование). В задумке — использование таблиц MyTables и  Fields для конструктора SQL-запросов (на русском языке).

 

Возможные направления работы в данный момент видятся следующие: это универсальные модули,  сценарии, которые можно использовать повсеместно и переносить из проекта в проект простым копированием:

  • Выбор даты/интервала дат
  • Управление акаунтами
  • Привилегии
  • Поиск в списке
  • Поиск в портале
  • Сортировка в списке / в портале
  • Выгрузка данных в текстовые форматы
  • Произвольная группировка
  • Навигация
  • Логирование изменений
  • Регулирование доступа к полям

Я призываю разработчиков обмениваться мнениями, практиками, наработками в этой области. А также предлагать идеи для разработок полезных универсальных модулей и сценариев. Результатом обсуждения, экспериментов, эксплуатации должно стать нечто в виде базы знаний “Лучшие практики”.

Благодарю за внимание.

 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

50 − 42 =