logo

Введение в написание сценариев

Примечание
Перед началом работы со сценариями рекомендуется ознакомиться со стандартами написания программного кода на языке C#.

В данной статье вам будет представлен обучающий материал по написанию сценариев в системе ELMA 3. Предполагается, что вы уже знакомы с самой системой ELMA и отвечаете требованиям к разработчику сценариев.

ELMA API - инструмент для разработки сценариев
С версии 3.2.9.20379 в системе доступен ELMA API. Подробнее...

Начало работы. Ваш первый сценарий

Перейдем сразу к делу. Для успешного обучения лучший метод – это практика.

Для написания сценариев в курсе обучения можно использовать Бесплатную Демо-версию системы.

Работа со сценариями ведется в Дизайнере ELMA. Сами сценарии используются во многих местах системы, наиболее же распространенным является использование их в операциях Сценарий бизнес-процессов.

Создадим наш первый сценарий. Для этого создадим бизнес-процесс и назовем его "Калькулятор". Имя класса для работы с контекстными переменными – P_Calculator. Также желательно задать соотвествующие названия классов для метрик процесса и для экземпляра процесса. Не забывайте, что классам, переменным, методам рекомендуется давать английские названия. Использование транслитерации и названий символов русского языка нежелательно, поэтому лучше сразу приучать себя к использованию английского языка в коде программы.

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

  • динамическая зона ответственности (так, чтобы все пользователи могли запускать процесс);
  • контекстные переменные:
    • Поле ввода (InputField) – дробное число, не пустое, по умолчанию = 0;
    • Текущий результат (CurrentResult) – дробное число, не пустое, по умолчанию = 0;
  • на диаграмме процесса расположите одну пользовательскую задачу и одну операцию Сценарий. В пользовательской задаче выведите контекстные переменные:
    • Поле ввода – для редактирования;
    • Текущий результат – для чтения.

Рис. 1. Графическая модель процесса "Калькулятор"

После этого можно приступать к написанию самого сценария. Для этого откройте настройки операции Сценарий и добавьте новый метод – Calculate. Текст сценария пока не заполняйте, нажмите кнопку Ок и потом на той же вкладке в настройках кнопку Перейти. При этом на карточке процесса откроется другая вкладка – Сценарии. На этой вкладке расположены все сценарии процесса. На данной вкладке используется компонент редактора SharpDevelop, он позволяет использовать автодополнение кода, что существенно упрощает разработку.

Для каждого процесса создается отдельный класс, наследующий базовый класс EleWise.ELMA.Workflow.Scripts.ProcessScriptBase<Context> (где Context – это класс контекста процесса, вы можете увидеть строку using Context = EleWise.ELMA.Model.Entities.ProcessContext.P_Calculator). В этом классе методы представляют собой отдельные сценарии. В нашем случае создается пустой метод Calculate, в который передается контекст процесса context:

public void Calculate(Context context)
{

}

Для начала сложим текущее значение и то, что пользователь ввел в поле ввода:

public void Calculate(Context context)
{
context.CurrentResult = context.CurrentResult + context.InputField;
}

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

Таким образом, вы видите, что любые изменения, внесенные в контекст в операции Сценарий сохраняются автоматически.

Но это, конечно, очень простой пример. Он показывает самые базовые операции, и цель его в том, чтобы познакомить пользователя с принципом работы со сценариями.

Второй пример сценария. Работа с объектами

Наиболее интересными, конечно, являются возможности по работе с объектами системы (иногда их называют сущности). Создадим в Дизайнере ELMA свой собственный объект Питомец (Pet) (будем вести учет домашних питомцев наших клиентов) в группе Работа с клиентами. Добавим в объект следующие свойства:

  • Имя (Name) – тип Строка, обязательно для заполнения, является наименованием, отображать в таблице. Рекомендуется добавлять поле с наименованием через добавление стандартных полей в справочнике. Для этого необходимо нажать правой кнопкой мыши в область, где описаны поля объекта, и выберать в контекстном меню пункт Добавить свойства по умолчанию, но в нашем примере сделаем это вручную;
  • Вид (Kind) – тип Выпадающий список: Кошка, Собака, Попугай, Рыбка, Черепаха, Змея;
  • Возраст (Age) – тип Целое число, может иметь пустое значение;
  • Хозяин (Master) – ссылка на объект Контакт, обязательно для заполнения, отображать в таблице.

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

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

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

  1. Создание записи объекта в сценарии.
  2. Проверка на наличие записи по определенному условию.

Создание записи объекта

Для выполнения первой части нам необходимо создать процесс "Добавление нового питомца" (NewPetAdding). Создадим для данного процесса следующие контекстные переменные:

  • Контакт (Contact) – ссылка на объект Контакт, является входной;
  • Имя питомца (PetName) – тип Строка;
  • Вид питомца (PetKind) – тип Выпадающий список: Кошка, Собака, Попугай, Рыбка, Черепаха, Змея;
  • Возраст питомца (PetAge) – тип Целое число;
  • Новый питомец (NewPet) – ссылка на объект Питомец;
  • Ошибка заполнения (FillingError) – тип Строка (нужна для вывода ошибки, если не выполнено какое-то условие).

После этого расположим на графической модели процесса 2 пользовательские задачи и один сценарий.

Рис. 2. Графическая модель процесса "Добавление нового питомца"

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

Теперь перейдем к редактированию сценария. Создайте новый метод CreateNewPet и вставьте туда следуюший код:

public void CreateNewPet(Context context)
{
    //Проверяем, что переменная Контакт выбрана
    if(context.Contact == null)
    {
        context.FillingError = "Не указан Контакт";
        //Выход из скрипта
        return;
    }
    
    //Проверяем, что Имя питомца не пустое
    if(String.IsNullOrWhiteSpace(context.PetName))
    {
        context.FillingError = "Не указано Имя питомца";
        //Выход из скрипта
        return;
    }
    
    //Создаем новую запись для объекта Питомец
    var newPet = EntityManager<Pet>.Create();
    
    //Присваиваем значения из контекста
    newPet.Name = context.PetName;
    newPet.Kind = context.PetKind;
    newPet.Age = context.PetAge;
    newPet.Master = context.Contact;
    
    //Сохраняем новую запись в БД
    newPet.Save();
    
    //Присваиваем новую запись в контекст
    context.NewPet = newPet;
}

Как видите, все достаточно понятно из комментариев. Единственный момент, который нужно прояснить, – это использование класса EntityManager<Pet>.

Менеджер сущности

Существует несколько типов менеджеров сущностей. Подробнее о типах менеджеров сущностей см. в соответствующей статье.

В нашем случае мы использовали статический класс для доступа к базовому типизированному менеджеру сущности. Сам класс EntityManager<T> – типизированный, и для доступа к конкретному менеджеру мы должны указать тип объекта (в нашем случае это как раз Питомец, имя класса у него Pet). После этого можно обратиться к статическому полю Instance, которое вернет объект менеджера сущности, или вызвать статический метод Create, который создаст новый объект указанного типа (в нашем случае Питомец) без записи в базу.

Далее идет присвоение значений свойств нового объекта из контекста процесса и сохранение объекта в БД при помощи метода Save.

Внимание!
При создании новой сущности всегда вызывайте Save для сохранения в базу. Но помните, сохранение происходит не сразу, а только по завершению всего сценария. Из этого следует, что если вы попытаетесь сохранить новую запись и сделать выборку по условию, то эта запись в выборку не попадет.
Всегда проверяйте, что обязательные поля объекта заполнены перед сохранением!

Попробуйте теперь опубликовать процесс: при этом отобразится сообщение об ошибке:

Тип "EleWise.ELMA.CRM.Models.IContact" определен в сборке, ссылка на которую отсутствует. Следует добавить ссылку на сборку "EleWise.ELMA.CRM, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null".

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

Рис. 3. Панель настройки. Кнопка "Добавить"

Далее в окне можно отфильтровать по имени сборки, например указать CRM в поле фильтра и после этого выбрать нужную сборку (в нашем случае это EleWise.ELMA.CRM, как указано в тексте ошибки).

Рис. 4. Диалоговое окно добавления ссылок на сборку

Ссылки на сборки
Вы можете добавлять ссылки на сборки системы ELMA или на глобальные сборки .NET, зарегистрированные в GAC.

Если вы все сделали правильно, то теперь вы можете запускать процесс (даже из карточки контакта, так как переменную Контакт мы пометили как входную) и создавать новые записи для питомцев. При этом, если какие-то из условий не будут выполнены, то ошибка будет записана и выведена пользователю в переменную.

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

Поиск записи по условию

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

public void CreateNewPet(Context context)
{
    //Проверяем, что переменная Контакт выбрана
    if(context.Contact == null)
    {
        context.FillingError = "Не указан Контакт";
        //Выход из скрипта
        return;
    }
    
    //Проверяем, что Имя питомца не пустое
    if(String.IsNullOrWhiteSpace(context.PetName))
    {
        context.FillingError = "Не указано Имя питомца";
        //Выход из скрипта
        return;
    }
    
    //Получаем список питомцев для указанного контакта и для указанного имени (полное совпадение)
    var pets = EntityManager<Pet>.Instance.Find(p => p.Master == context.Contact && p.Name == context.PetName);
    
    //Проверяем, есть ли в коллекции хоть одна запись
    if(pets.Any())
    {
        context.FillingError = "Уже есть питомец с указанным именем";
        //Выход из скрипта
        return;
    }
    
    //Создаем новую запись для объекта Питомец
    var newPet = EntityManager<Pet>.Create();
    
    //Присваиваем значения из контекста
    newPet.Name = context.PetName;
    newPet.Kind = context.PetKind;
    newPet.Age = context.PetAge;
    newPet.Master = context.Contact;
    
    //Сохраняем новую запись в БД
    newPet.Save();
    
    //Присваиваем новую запись в контекст
    context.NewPet = newPet;
}

Как видите, здесь мы снова обращаемся к менеджеру сущности, но на этот раз используем метод Find для поиска по выражению.

Больше информации о возможности поиска сущностей вы можете найти в соответствующей статье.

В нашем случае мы ищем все объекты типа Питомец (Pet), где поле Хозяин (Master) совпадает с переменной Контакт, и поле Имя (Name) равно переменной Имя питомца в контексте процесса. Мы можем использовать поиск по выражению, так как у нас используются только условия сравнения. Далее проверяется наличие записи в результирующей коллекции. В нашем случае, если найдется хоть одна запись удовлетворяющая условию, то нужно вывести сообщение об ошибке.

Как видите, мы решили поставленную задачу по добавлению питомца для выбранного контакта при помощи достаточно простого скрипта. Далее мы рассмотрим более сложный пример.

Третий пример сценария. Редактирование и выпадающие списки

В этом примере мы будем использовать тот же процесс и объект, что и в предыдущем, но теперь при выборе контакта мы будем выводить список питомцев пользователю на форму. Также добавим возможность прямо на форме задаче редактировать данные питомца. Для выбора питомца создадим переменную Питомец (Pet) типа Выпадающий список с возможностью ввода своих вариантов. В этот список мы будем заносить уже существующих питомцев для контакта и давать редактировать возраст, либо сразу дадим добавить питомца.

Чтобы корректно обработать все варианты (ведь процесс может быть запущен с уже указанным контактом на входе), нужно добавить еще один сценарий. Данный сценарий должен быть расположен сразу после стартового события, функцию назовем OnStartProcess:

public void OnStartProcess(Context context)
{
    FillPetDDL(context);
}
private void FillPetDDL(Context context)
{
    //Получаем настройки выпадающего списка для контекстной переменной Питомец
    var settings = (DropDownListSettings) context.GetSettingsFor(c => c.Pet);
    
    //Очищаем все элементы
    settings.Items.Clear();
    
    if(context.Contact == null)
    {
        //Если контакт не выбран - дальше ничего не делаем
        return;
    }
    
    //Находим всех питомцев, для выбранного контакта
    var pets = EntityManager<IPet>.Instance.Find(p => p.Master == context.Contact);
    
    foreach (var pet in pets)
    {
        //Добавляем питомца в выпадающий список
        settings.Items.Add(new DropDownItem(pet.Id.ToString(), pet.Name));
    }
    
    //Сохраняем настройки выпадающего списка
    settings.Save();

     //Обнуляем ранее выбранное значение переменной Питомец
    context.Pet = null;
}

Как видите, тут мы используем определение дополнительной функции FillPetDDL, в которой производим заполнение выпадающего списка Питомец (данная функция понадобится нам еще раз в дальнейшем для обработки событий на форме). Обратите внимание, что настройки выпадающего списка привязаны к контексту процесса, а значит, для каждого экземпляра они будут свои.

Следующим шагом необходимо настроить форму первой задачи в процессе так, чтобы можно было создавать либо редактировать связанных с контактом питомцев на одном шаге. Для этого добавим на форму переменные Контакт, Питомец, Вид питомца и Возраст питомца. Далее настроим для формы скрипт при загрузке (функция OnPetCreateFormLoad):

public void OnPetCreateFormLoad(Context context, EleWise.ELMA.Model.Views.FormViewBuilder<Context> form)
{
    var pet = GetPet(context);
    
    if(pet != null)
    {
        //Устанавливаем признак Только для чтения на форме для свойства Вид питомца
        form.For(c => c.PetKind).ReadOnly(true);
    }
}
private Pet GetPet(Context context)
{
    if(context.Contact == null)
    {
        return null;
    }
    
    var pName = context.Pet.Value;
    if(string.IsNullOrWhiteSpace(pName))
    {
        return null;
    }
    
    var pet = EntityManager<Pet>.Instance.Find(p => p.Master == context.Contact && p.Name == pName).FirstOrDefault();
    
    return pet;
}

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

Далее нам нужно настроить поведение переменных на форме. Для этого зайдем в Настройки задачи и добавим для переменных на форме сценарии при изменении. Для переменной Контакт (функция OnContactSelect):

public void OnContactSelect(Context context, EleWise.ELMA.Model.Views.FormViewBuilder<Context> form)
{
    FillPetDDL(context);
    
    form.For(c => c.PetKind).ReadOnly(false);
    context.PetKind = null;
    context.PetAge = null;
}

и для переменной Питомец (функция OnPetSelect):

public void OnPetSelect(Context context, EleWise.ELMA.Model.Views.FormViewBuilder<Context> form)
{
    if(context.Contace == null)
    {
        return;
    }
    
    //Выбираем питомца
    var pet = GetPet(context);
    
    if(pet != null)
    {
        //Если уже сохранен в справочнике, то заполняем поля контекста
        context.PetName = pet.Name;
        context.PetAge = pet.Age;
        context.PetKind = pet.Kind;
        context.NewPet= (Pet)pet;
        
        //Запрещаем редактирование переменной Вид питомца
        form.For(c => c.PetKind).ReadOnly(true);
    }
    else
    {
        //Иначе сбрасываем на пустые значения и устанавливаем имя из выпадающего списка
        context.PetName = context.Pet.Value;
        context.PetAge = 0;
        context.PetKind = null;
        context.NewPet = null;
        
        //Разрешаем редактирование переменной Вид питомца
        form.For(c => c.PetKind).ReadOnly(false);
    }
}

Теперь при выборе другого контакта значения в полях будут сбрасываться на пустые, а список Питомец – заполняться связанными данными из справочника. При выборе же из списка питомца мы увидим, что обновились данные в полях Вид и Возраст, а поле Вид станет недоступным для редактирования.

Рис. 5. Форма задачи "Ввести данные о питомце". Автозаполнение полей при выборе питомца из выпадающего списка

Если же ввести в поле Питомец имя, которого еще нет у данного контакта, то поле Вид будет снова доступно для редактирования.

Рис. 6. Форма задачи "Ввести данные о питомце". Добавление нового питомца

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

После всех изменений нам нужно только немного переписать сценарий сохранения питомца:

public void CreateNewPet(Context context)
{
    //Проверяем, что переменная Контакт выбрана
    if(context.Contact == null)
    {
        context.FillingError = "Не указан Контакт";
        //Выход из скрипта
        return;
    }
    
    //Проверяем, что Имя питомца не пустое
    if(String.IsNullOrWhiteSpace(context.PetName))
    {
        context.FillingError = "Не указано Имя питомца";
        //Выход из скрипта
        return;
    }
    
    context.NewPet = null;
    
    //Получаем питомца для указанного контакта и для указанного имени (полное совпадение)
    var pet = GetPet(context);
    
    if(pet != null)
    {
        //Если питомец найден, присваиваем его в контекст
        context.NewPet = pet;
    }
    
    //Проверяем переменную в контексте
    if(context.NewPet == null)
    {
        //Создаем новую запись для объекта Питомец
        var newPet = EntityManager<Pet>.Create();
        
        //Присваиваем значения из контекста
        newPet.Name = context.PetName;
        newPet.Kind = context.PetKind;
        newPet.Age = context.PetAge;
        newPet.Master = context.Contact;
        
        //Сохраняем новую запись в БД
        newPet.Save();
        
        //Присваиваем новую запись в контекст
        context.NewPet = newPet;
    }
    else
    {
        //Обновляем только возраст питомца
        context.NewPet.Age = context.PetAge;
        context.NewPet.Save();
    }
}

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

Что почитать дальше

Изучив эти три примера, вы уже сможете самостоятельно реализовать очень много сценариев в системе. Комбинируя сценарии на форме и сценарии между задачами, можно обрабатывать самые сложные бизнес-задачи.

Для более глубокого изучения работы со сценариями и разбора примеров рекомендуем ознакомиться со статьями: