logo

[ELMA3] Создание веб-модуля для приложения

Перед созданием модуля ознакомьтесь со страницей Создание модуля для приложения.

Представление данных в веб-приложении

Основной целью создания модуля в нашем случае является предоставление пользователю информации в веб-приложении. В качестве основы модульной системы используется Orchard CMS версии 1.0.

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

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

У нас уже есть созданный проект в модуле, отвечающий за веб-часть. Информация о нашем модуле для ядра CMS Orchard хранится в файле Module.txt.

Подробнее о структуре работы модулей Orchard можно прочитать на сайте http://docs.orchardproject.net/en/latest/Documentation/Builtin-Features/.

Не забудьте указать зависимости от базовых модулей системы (см. статью Структура веб-модулей).

Все маршруты для нашего модуля уже прописаны в файле RouteProvider.cs.

Пункты меню

Поскольку у нас будет собственная логика, нам необходима точка входа на наши страницы. Для этого лучше всего использовать меню системы. Мы можем реализовать точку расширения EleWise.ELMA.Web.Content.Menu.IMenuItemsProvider.

Для начала нам необходимо определить, какие пункты у нас должны быть. Мы сделаем двухуровневое меню: на верхнем уровне будет пункт Работа с клиентами, и внутри него будет пункт Клиенты. В левом меню пункты верхнего уровня имеют иконку 24х24, пункты 2-го уровня иконку 16х16.

/// <summary>
    /// Провайдер элементов меню
    /// </summary>
    [ExtensionPoint(ServiceScope.Shell)]
    public interface IMenuItemsProvider
    {
        /// <summary>
        /// Построить элементы меню
        /// </summary>
        /// <param name="factory">Фабрика элементов</param>
        void Items(MenuItemFactory factory);

        /// <summary>
        /// Локализованные названия элементов
        /// </summary>
        List<string> LocalizedItemsNames { get; }

        /// <summary>
        /// Локализованные описания элементов
        /// </summary>
        List<string> LocalizedItemsDescriptions { get; }

    }

Для реализации нам нужно добавить в построитель меню 2 элемента и связать их друг с другом. Для примера можно посмотреть, как это сделано в модуле Управление проектами:

[Component]
	public class ProjectMenuItems : IMenuItemsProvider
    {
        public const string Projects = "ProjectsModule";
        public const string ProjectsList = "ProjectsList";
        public const string Milestones = "ProjectMilestones";

        public const string PROJECT_ADMIN_MENU = "project-admin-menu";

        public void Items(MenuItemFactory factory)
        {

#region left

            factory.Section("Проекты", Projects)
                .Image32(RouteProvider.ImagesFolder + "x32/Project.png")
				.Order(100)
                .Container("left")
                .OnTop(true)
                .Stretch(true);

            factory.Action(new ActionRoute("Index", "ProjectList", new { area = RouteProvider.AreaName }), ObjectIconFormat.x16)
                .Name("Список проектов")
                .Code(ProjectsList)
                .Parent(Projects)
                .Order(10)
                .Container("left");

            factory.Action(new ActionRoute("Index", "Milestone", new { area = RouteProvider.AreaName }), ObjectIconFormat.x16)
                .Name("Контрольные точки")
                .Code(Milestones)
                .Parent(Projects)
                .Order(20)
                .Container("left");

#endregion

            factory.Action(new ActionRoute("Index", "Admin", new { area = RouteProvider.AreaName }), ObjectIconFormat.x16)
               .Code(PROJECT_ADMIN_MENU)
               .Order(105)
               .Parent("admin")
               .Container("left").Copy(b => b.Container("top"));
        }


        public List<string> LocalizedItemsNames
        {
            get {
                return new List<string> { 
                    SR.T("Проекты"),
                    SR.T("Контрольные точки"),
                    SR.T("Список проектов")
                };
            }
        }

        public List<string> LocalizedItemsDescriptions
        {
            get { return null; }
        }
    }

По аналогии мы можем добавить пункты меню для нашего модуля

[Component]
    public class CRMExtensionMenuItems : IMenuItemsProvider
    {
        public void Items(MenuItemFactory factory)
        {
            factory.With(f => f.Container("left"));

            factory.Section("Работа с клиентами", "crm")
                .Order(100)
                .Image24("#x24/crm.gif");

            factory.With(f => f.Container("left").Parent("crm"));

            factory.Action(new ActionRoute("Index", "Company", new { area = RouteProvider.AreaName }));
        }

        public List<string> LocalizedItemsNames
        {
            get
            {
                return new List<string> { 
                SR.T("Работа с клиентами"),
                SR.T("Клиенты")
            };
            }
        }

        public List<string> LocalizedItemsDescriptions
        {
            get { return null; }
        }
    }
Обратите внимание
  1. В качестве действия для пункта меню мы указали настройки перехода на действие контроллера – экземпляр класса ActionRoute, чтобы все заработало, далее метод контроллера нужно отметить атрибутом EleWise.ELMA.Web.Content.ContentItemAttribute, ниже будет показан пример.
  2. Все картинки должны находиться по пути «~Web\Content\Images».


Создание сущности

Пришло время перейти к созданию новой сущности. Для этого создадим новый контроллер:

[ActionLinkArea(RouteProvider.AreaName)]
    public class CompanyController : FilterTreeBaseController<ICompany, long>
    {
    }

Добавляем новое действие:

public ActionResult Create()
        {
            var company = InterfaceActivator.Create<ICompany>();
            return View(company);
        }

Тут мы создаем новый экземпляр нашей сущности и передаем в представление.

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

Теперь необходимо создать представление. Для этого в папке Views\Company создаем представление Create.cshtml (используем разметку Razor):

@using ViewType = EleWise.ELMA.Model.Views.ViewType
@model EleWise.ELMA.CRMExtension.Models.ICompany


@{
    Html.Header(SR.T("Создать компанию"));
}

@{
    @(Html.Toolbar().Group()
        .ToolbarSubmit(EleWise.ELMA.SR.Save, "#x32/Save.png")
        .ToolbarLink(EleWise.ELMA.SR.Cancel, "#x32/Cancel.png", "javascript:history.back(-1);"))
}

@using (Html.ElmaForm())
{
    @Html.ValidationSummary(false)
    @Html.BuildFormForModel(Model, ViewType.Create,
        q =>
        {
            q.HideForm();
            q.HideAllProperties();
            q.PropertyRow(m => m.Name);
            q.PropertyRow(m => m.INN);
            q.PropertyRow(m => m.KPP);
        })
}

В результате получим следующее:

Важные вещи, на которые необходимо обратить внимание:

  • для установки заголовка страницы используйте Html.Header;
  • для обрамления в общую форму используйте Html.ElmaForm;
  • для вывода свойств сущностей используйте Html.BuildForForModel, внутри которого вызывайте PropertyRow. Это поможет вам вывести объект в таком же виде, в каком отображаются системные объекты;
  • используйте Html.Toolbar для отображения кнопок действия.

Для сохранения сущности Компания определим в контроллере действие:

[HttpPost]
        public ActionResult Create(ICompany company)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    Manager.Save(company);

                    Notifier.Information(SR.T("Компания \"{0}\" сохранена успешно", company.Name));

                    return RedirectToAction("Index");
                }

                return View(company);
            }
            catch (Exception ex)
            {
                Notifier.Error(ex.Message);
                return View(company);
            }
        }

Здесь мы используем свойство Manager и Notifier из базового класса. Как видите, контроллер снова очень прост и не содержит никакой сложной логики, кроме обработки ошибок. На самом деле мы даже можем не делать обработку ошибок в этом методе, так как в базовом классе уже установлен обработчик на все методы, и при вызове ошибки он отобразит ее точно так же. Вызов Manager.Save(company) производит сохранение сущности в базу.

Просмотр сущности

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

public ActionResult Details(long id)
        {
            var company = Manager.Load(id);
            return View(company);
        }

Добавляем для этого действия представление Details.cshtml:

@model EleWise.ELMA.CRMExtension.Models.ICompany
@using EleWise.ELMA.CRMExtension.Web.Controllers
@using ViewType = EleWise.ELMA.Model.Views.ViewType

@{
    Html.Header(SR.T("Компания - {0}", Model.Name));
}

@(Html.Toolbar().Group("edit-button").ToolbarButton(
new ActionToolbarItem("toolbar-button-Edit")
{
    Text = SR.Edit,
    ToolTip = "Перейти к странице редактирования компании",
    IconUrl = "#x32/edit.png",
    Url = Url.Action<CompanyController>(c => c.Edit(Model.Id))
}
))

@Html.BuildFormForModel(Model, ViewType.Display,
    q =>
    {
        q.HideForm();
        q.HideAllProperties();
        q.PropertyRow(m => m.Name);
        q.PropertyRow(m => m.CreationAuthor);
        q.PropertyRow(m => m.CreationDate);
        q.PropertyRow(m => m.ChangeAuthor);
        q.PropertyRow(m => m.ChangeDate);
        q.PropertyRow(m => m.INN);
        q.PropertyRow(m => m.KPP);
    })

В результате получим следующее:

Здесь мы используем Html.Toolbar() для добавления действия редактирования на форму просмотра сущности.

Редактирование сущности

Теперь нам нужно реализовать редактирование сущностей. Для этого в контроллер нужно добавить новое действие:

public ActionResult Edit(long id)
        {
            var example = Manager.Load(id);

            return View(example);
        }

Добавим для этого действия представление Edit.cshtml:

@using ViewType = EleWise.ELMA.Model.Views.ViewType
@model EleWise.ELMA.CRMExtension.Models.ICompany


@{
    Html.Header(SR.T("Редактирование компании - {0}", Model.Name));
}

@{
    @(Html.Toolbar().Group()
        .ToolbarSubmit(EleWise.ELMA.SR.Save, "#x32/Save.png")
        .ToolbarLink(EleWise.ELMA.SR.Cancel, "#x32/Cancel.png", "javascript:history.back(-1);"))
}

@using (Html.ElmaForm())
{
    @Html.ValidationSummary(false)
    @Html.BuildFormForModel(Model, ViewType.Edit,
        q =>
        {
            q.HideForm();
            q.HideAllProperties();
            q.PropertyRow(m => m.Name);
            q.PropertyRow(m => m.INN);
            q.PropertyRow(m => m.KPP);
        })
}

В результате получим следующее:

Для сохранения отредактированной сущности определим в контроллере действие:

[HttpPost]
        public ActionResult Edit(ICompany company)
        {
            company.Save();

            Notifier.Information(SR.T("Компания {0} сохранена успешно", company.Name));

            return RedirectToAction("Details", new { id = company.Id });
        }

После успешного сохранения вас перекинет на страницу просмотра сущности.

Операции с сущностью (бизнес-логика)

Просмотр списка, организация поиска

Для вывода списка сущностей и фильтрации по нему нам понадобится добавить новое действие:

[ContentItem(Name = "Клиенты", Image16 = "#x16/crm_clients.gif", Order = 0)]
        public ActionResult Index()
        {

            var filterModel = CreateFilter();

            filterModel.Filter = new InstanceOf<ICompanyFilter>()
            {
                New =
                {
                    PermissionId = CRMExtensionPermissionProvider.ViewCompanyPermission.Id
                }
            }.New;

            var list = CreateGridData(new GridCommand(), filterModel);

            return View(list);
        }

Функция CreateFilter() создает объект FilterModel, предназначенный для отображения фильтра на представлении Grid.cshtml в привычном нам виде. Подробнее будет написано ниже.

Класс InstanceOf используется, как и InterfaceActivator, для создания реальных объектов из интерфейса. Посмотрим, какое представление Index.cshtml нам нужно создать для представления списка:

@using EleWise.ELMA.CRMExtension.Web.Controllers
@model IGridData<EleWise.ELMA.CRMExtension.Models.ICompany>

    @{
        Html.Header(SR.T("Компания"));
    }

    @{
        @(Html.Toolbar().Group()
                .ToolbarLink(SR.Add, "#x32/Add.png", Url.Action<CompanyController>(c => c.Create())))
    }

    @Html.Partial("Grid")

В самом конце вы видите вызов отображения другого представления Grid.cshtml:

@using EleWise.ELMA.BPM.Web.Common.Extensions
@model EleWise.ELMA.BPM.Web.Common.Models.GridDataFilter<EleWise.ELMA.CRMExtension.Models.ICompany>

    @Html.Partial("Filter/FilterModel", Model.DataFilter.Builder()
    .SubmitText(SR.T("Найти компании"))
    .GridAction(Url.Action("Grid"))
        .ApplayFilterScript(string.Format("applyFilterGrid('{0}', 'filteringGridForm')", Model.GridId))
    .FilterUrl(Url.Action("Index") + "?FilterId=")
    .Model,
    new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "DataFilter" } })

    @(Html.FilterDynamicGrid(Model)
        .Columns(c =>
        {
            c.For(m => m.Name).Link(m => Url.Action("Details", "Company", new { id = m.Id }));
            c.For(m => m.CreationAuthor);
            c.For(m => m.CreationDate);
            c.For(m => m.ChangeAuthor);
            c.For(m => m.ChangeDate);
            c.For(m => m.INN);
            c.For(m => m.KPP);
        })
        .Action("Grid"))

Здесь DataFilter имеет тип FilterModel. С помощью функции Builder заполняем необходимые поля для создания экземпляра FilterModel для отправки на представление FilterModel.cshtml, которое нарисует фильтр в привычном для нас виде. Результат:

Обратите внимание на указание действия Action("Grid"). Оно позволит обращаться напрямую к действию Grid в контроллере.

Для завершения работы с таблицей необходимо в контроллер добавить действие:

[CustomGridAction]
  public override ActionResult Grid(GridCommand command, [Bind(Prefix = "DataFilter")]FilterModel filter, long? FilterId = null, string searchTasksType = null)
  {
   if (filter == null)
   {
    filter = CreateFilter(filterToMerge: InterfaceActivator.Create<ICompanyFilter>());
   }
   else
   {
    filter.Filter.PermissionId = CRMExtensionPermissionProvider.ViewCompanyPermission.Id;
   }

   var data = CreateGridData(command, filter);

   return PartialView(data);
  }

Это действие будет вызываться при сортировке, переходу по страницам и других действиях. Атрибут параметра filter указывает на то, какой префикс использовать при присвоении данных. Обратите внимание, ключевое слово override здесь обязательно, как и не используемые параметры по умолчанию FilterId и searchTasksType. Без этих параметров указание override вызовет ошибку при компиляции, а отсутствие override не даст вам сохранить объект (из-за ошибки неявно указанной функции Grid).

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

  1. EleWise.ELMA.SDK.
  2. EleWise.ELMA.SDK.Web.
  3. EleWise.ELMA.Security.
  4. EleWise.ELMA.BPM.
  5. EleWise.ELMA.BPM.Web.Common.

Смотри также: