Создание серверного расширения Web-клиента

В описателе элемента управления был объявлен конвертер для значения элемента — RefCasesConverter (свойство Binding), который должен приводить настоящее значение (идентификатор строки Дела в Справочнике номенклатуры дел 5) к виду, удобному для пользователя — в данном примере к строке вида: Год. Название разделов. Название дела.

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

Сервис для работы со Справочником номенклатуры дел 5

Прежде всего необходимо реализовать сервис для работы со Справочником номенклатуры дел 5, методы которого должны:

  • Возвращать список лет.

  • Возвращать год для определённой секции — нужно, если в программе Конструктор Web-разметок был выбран раздел, из которого выбираются дела.

  • Возвращать все разделы для определённого года, а если требуется, только разделы из корневого раздела, выбранного в программе Конструктор Web-разметок.

  • Возвращать все дела из определённого раздела.

  • Возвращать отображаемое название дела — для конвертера значений RefCasesConverter.

  • Возвращать список дел с искомым названием, а если требуется, только дела из корневого раздела, выбранного в программе Конструктор Web-разметок.

Ниже представлен интерфейс сервиса, описывающего установленные требования. Первые два условия для сервиса объединены в один метод GetYears.

Файл RefCasesServerExtension\Services\IRefCasesService.cs
using RefCasesServerExtension.Models;
using System;
using System.Collections.Generic;

namespace RefCasesServerExtension.Services
{
    public interface IRefCasesService
    {
        List<Year> GetYears(Guid? rootSection = null);
        List<Section> GetSections(Guid yearID, Guid? rootSection = null);
        List<Case> GetCases(Guid sectionID);
        string GetCaseTitle(Guid caseID);
        List<CaseClientModel> SearchCases(string caseName, Guid? rootSection = null);
    }
}

И реализация сервиса IRefCasesService:

Файл RefCasesServerExtension\Services\RefCasesService.cs
using DocsVision.Platform.ObjectManager;
using DocsVision.Platform.WebClient;
using DocsVision.Platform.WebClient.Managers;
using RefCasesServerExtension.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace RefCasesServerExtension.Services
{
    public class RefCasesService : IRefCasesService
    {

        private Guid refCasesId = new Guid("246197EA-846A-44DA-9EA3-0BCAE5500388"); (1)
        private Guid yearsSectionId = new Guid("BD44C786-6E41-450E-BD60-66919657E51B"); (2)
        private Guid sectionsSectionID = new Guid("319E425D-543C-45DB-BD51-955B58476EDB"); (3)
        private Guid caseSectionID = new Guid("56af8231-b918-42d4-ac15-90ec2e9a0725"); (4)

        (5)
        private const string Year = "Year";
        private const string DisplayIndex = "DisplayIndex";
        private const string Case_Name = "Case_Name";
        private const string Case_Index = "Case_Index";
        private const string Case_SectionDisplayIndex = "Case_SectionDisplayIndex";
        private const string Name = "Name";

        ICurrentObjectContextProvider objectContextProvider;
        private AdvancedCardManager cardManager => objectContextProvider.GetOrCreateCurrentSessionContext().AdvancedCardManager;

        public RefCasesService(ICurrentObjectContextProvider objectContextProvider) => this.objectContextProvider = objectContextProvider;

        public List<Year> GetYears(Guid? rootSection) (6)
        {
            CardData refCasesData = cardManager.GetDictionaryData(refCasesId);
            SectionData yearSection = refCasesData.Sections[yearsSectionId];

            if (rootSection.HasValue && refCasesData.Sections[sectionsSectionID].RowExists(rootSection.Value)) (7)
            {
                var sectionRow = refCasesData.Sections[sectionsSectionID].GetRow(rootSection.Value);
                var yearRow = sectionRow.SubSection.ParentRow;

                return new List<Year>() {
                    new Year() { ID = yearRow.Id, DisplayValue = yearRow[Year].ToString() }
                };
            }

            return yearSection.Rows.Select<RowData, Year>(row => new Year() (8)
            {
                ID = row.Id,
                DisplayValue = row[Year].ToString()
            }).ToList();
        }

        public List<Section> GetSections(Guid yearID, Guid? rootSection) (9)
        {
            CardData refCasesData = cardManager.GetDictionaryData(refCasesId);
            SectionData sectionsSection = refCasesData.Sections[sectionsSectionID];

            if (rootSection.HasValue && sectionsSection.RowExists(rootSection.Value)) (10)
            {
                RowData row = sectionsSection.GetRow(rootSection.Value);
                return GetSectionsFromRows(new List<RowData>() { row });
            }

            SectionData yearSection = refCasesData.Sections[yearsSectionId];

            if (yearSection.RowExists(yearID)) (11)
            {
                RowDataCollection sectionRows = yearSection.GetRow(yearID).ChildSections[sectionsSectionID].Rows;
                return GetSectionsFromRows(sectionRows);
            }

            return new List<Section>();
        }

        public List<Case> GetCases(Guid sectionID) (12)
        {
            CardData refCasesData = cardManager.GetDictionaryData(refCasesId);
            SectionData sectionsSection = refCasesData.Sections[sectionsSectionID];

            if (sectionsSection.RowExists(sectionID))
            {
                RowDataCollection sectionRows = sectionsSection.GetRow(sectionID).ChildSections[caseSectionID].Rows;
                return GetCasesFromRows(sectionRows);
            }

            return new List<Case>();
        }

        public string GetCaseTitle(Guid caseID) (13)
        {
            CardData refCasesData = cardManager.GetDictionaryData(refCasesId);
            SectionData section = refCasesData.Sections[caseSectionID];

            if (section.RowExists(caseID))
            {
                RowData caseRow = section.GetRow(caseID); (14)
                var sectionRow = caseRow.SubSection.ParentRow;
                var yearRow = sectionRow.SubSection.ParentRow;

                return string.Format("{0}, {1}, {2}", yearRow[Year], sectionRow[DisplayIndex], caseRow[Case_Name]);
            }

            return "Ошибка!";
        }

        public List<CaseClientModel> SearchCases(string caseName, Guid? rootSection = null) (15)
        {
            CardData refCasesData = cardManager.GetDictionaryData(refCasesId);

            RowDataCollection allRows;

            if (rootSection.HasValue && refCasesData.Sections[sectionsSectionID].RowExists(rootSection.Value)) (16)
                allRows = refCasesData.Sections[caseSectionID].GetAllRows(rootSection.Value, true);
            else
                allRows = refCasesData.Sections[caseSectionID].GetAllRows();

            var results = new List<CaseClientModel>();

            foreach (var caseRow in allRows)
            {
                if (caseRow[Case_Name].ToString().IndexOf(caseName, StringComparison.InvariantCultureIgnoreCase) > -1 ||
                    (caseRow[Case_SectionDisplayIndex].ToString() + "-" + caseRow[Case_Index].ToString()).IndexOf(caseName, StringComparison.InvariantCultureIgnoreCase) > -1) (17)
                {
                    var sectionRow = caseRow.SubSection.ParentRow;
                    var yearRow = sectionRow.SubSection.ParentRow;

                    results.Add(new CaseClientModel()
                    {
                        Id = caseRow.Id,

                        Name = string.Format("{0}, {1}, {2}", yearRow[Year], sectionRow[DisplayIndex], caseRow[Case_Name]) (18)
                    });
                }
            }

            return results;
        }


        List<Section> GetSectionsFromRows(IEnumerable<RowData> rows) (19)
        {
            var nodes = new List<Section>();

            foreach (var row in rows)
            {
                var node = new Section() { ID = row.Id, DisplayValue = row[Name].ToString() };
                if (row.HasChildRows)
                    node.Sections = GetSectionsFromRows(row.ChildRows);

                nodes.Add(node);
            }
            return nodes;
        }

        List<Case> GetCasesFromRows(RowDataCollection rows) (20)
        {
            var nodes = new List<Case>();

            foreach (var row in rows)
            {
                var node = new Case() { ID = row.Id, DisplayValue = row[Case_Name].ToString() };
                if (row.HasChildRows)
                    node.Cases = GetCasesFromRows(row.ChildRows);

                nodes.Add(node);
            }
            return nodes;
        }
    }
}
1 Справочник номенклатуры дел 5.
2 Секция Года справочника.
3 Секция Разделы справочника.
4 Секция Дела справочника.
5 Названия полей.
6 Возвращает список лет из Справочника номенклатуры дел 5.
7 Если выбран корневой раздел, нужно вернуть только его Год.
8 Иначе возвращаем все Года.
9 Возвращает дерево разделов указанного Года из Справочника номенклатуры дел 5.
10 Если выбран корневой раздел, вернуть только его подразделы.
11 Иначе вернуть все разделы указанного Года.
12 Вернуть все дела указанного Раздела.
13 Вернуть отображаемое название Дела.
14 Получаем для дела родительские строки раздела и Года.
15 Поиск дела по названию и по идентификатору Дела.
16 Если указан коневой Раздел, поиск только в нём, иначе — во всех Разделах.
17 Проверяем название Раздела — поле Case_Name.
18 Возвращаем сразу отображаемое название.
19 Возвращаем список Разделов для строк секции справочника.
20 Возвращаем список Дел для строк справочника.

Конвертер "RefCasesConverter"

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

Для разрабатываемого элемента управления название конвертера (RefCasesConverter) было объявлено в описателе элемента управления. Теперь его нужно реализовать и зарегистрировать в серверном расширении Web-клиента.

В данном пример конвертер использует метод IRefCasesService.GetCaseTitle для получения отображаемого названия Дела.

Файл RefCasesServerExtension\BindingConverters\RefCasesConverter.cs
using DocsVision.WebClientLibrary.Layout.IL;
using DocsVision.WebClientLibrary.ObjectModel;
using DocsVision.WebClientLibrary.ObjectModel.Services.BindingConverters;
using DocsVision.WebClientLibrary.ObjectModel.Services.LayoutModel;
using RefCasesServerExtension.Models;
using RefCasesServerExtension.Services;
using System;

namespace RefCasesServerExtension.BindingConverters
{
    public class RefCasesConverter : BaseBindingConverter (1)
    {
        private IRefCasesService refCasesService;

        public RefCasesConverter(IServiceProvider serviceProvider, IRefCasesService refCasesService) : base(serviceProvider, "RefCasesConverter") (2)
        {
            this.refCasesService = refCasesService; (3)
        }

        public override BindingResult ConvertForDisplay(ControlContext controlContext, LayoutBinding binding, BindingResult bindingResult) (4)
        {
            var itemId = bindingResult.Value != null ? (Guid)bindingResult.Value : Guid.Empty;
            var name = itemId == Guid.Empty ? "" : refCasesService.GetCaseTitle(itemId);

            var model = new CaseClientModel() { Id = itemId, Name = name }; (5)

            return bindingResult.Clone(model);
        }
    }
}
1 Класс конвертера может быть производным от BaseBindingConverter или полностью реализовывать интерфейс IBindingConverter.
2 В базовый класс нужно передать название конвертера, указанного в описателе RefCasesConverter.
3 Получаем реализованный ранее сервис для работы со Справочником номенклатуры дел 5.
4 Основной метод, возвращающий отображаемое значение (а точнее модель) для значения элемента управления — bindingResult.Value.
5 Клиент ожидает модель, включающую идентификатор и название Дела.

Веб-методы серверного расширения

Клиентской части элемента управления нужны данные из Справочника номенклатуры дел 5: для показа списка лет, разделов, а также дел. Предоставить доступ к данным можно с помощью веб-методов серверного расширения.

Далее приведён код контроллера, предоставляющего такие веб-методы.

Файл RefCasesServerExtension\Controllers\RefCasesOperationController.cs
using RefCasesServerExtension.Models;
using RefCasesServerExtension.Services;
using System;
using System.Linq;
using DocsVision.Platform.WebClient.Models;
using DocsVision.Platform.WebClient.Models.Generic;
using Microsoft.AspNetCore.Mvc;

namespace RefCasesServerExtension.Controllers
{
    public class RefCasesOperationController : ControllerBase
    {
        private readonly IRefCasesService refCasesService;

        public RefCasesOperationController(IRefCasesService refCasesService) (1)
        {
            this.refCasesService = refCasesService;
        }

        [HttpPost]
        public Year[] GetYears([FromQuery]Guid? rootSectionID) (2)
        {
            return refCasesService.GetYears(rootSectionID).ToArray();
        }

        [HttpPost]
        public Section[] GetSections([FromQuery]Guid yearID, [FromQuery]Guid? rootSectionID) { (3)
            return refCasesService.GetSections(yearID, rootSectionID).ToArray();
        }

        [HttpPost]
        public Case[] GetCases([FromQuery]Guid sectionID) (4)
        {
            return refCasesService.GetCases(sectionID).ToArray();
        }

        [HttpPost]
        public string GetCaseDisplayName([FromQuery]Guid caseID) (5)
        {
            return refCasesService.GetCaseTitle(caseID);
        }

        [HttpPost]
        public CaseSearchResult SearchCase([FromQuery]string caseName, [FromQuery]int skipCount, [FromQuery]int maxCount, [FromQuery]Guid? rootSectionID) (6)
        {
            var rows = refCasesService.SearchCases(caseName, rootSectionID); (7)

            var result = new CaseSearchResult
            {
                Items = rows.Skip(skipCount).Take(maxCount).ToArray(), (8) (9)

                HasMore = rows.Count > skipCount + maxCount
            }; (10)

            return result;
        }
    }
}
1 В конструкторе контроллера получаем ссылку на реализованный сервис для работы со Справочником номенклатуры дел 5.
2 Возвращает список лет.
3 Возвращает список разделов.
4 Возвращает список дел.
5 Возвращает отображаемое название дела.
6 Поиск дела по имени.
7 Получаем все подходящие дела.
8 Оставляем только количество запрошенных клиентом.
9 Т. к. количество результатов поискового запроса SearchCase может быть достаточно большим, клиенту предоставлена возможность ограничивать его с помощью параметров skipCount (количество пропускаемых результатов) и maxCount (максимальное количество результатов, принимаемых клиентом за один раз).
10 Устанавливаем флаг наличия дополнительных результатов.

Ядро серверного расширения

Реализованные сущности необходимо зарегистрировать в серверном расширении.

Файл RefCasesServerExtension\RefCasesServerExtension.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Resources;
using Autofac;
using Autofac.Extras.Ordering;
using DocsVision.WebClient.Extensibility;
using DocsVision.WebClient.Helpers;
using DocsVision.WebClientLibrary.ObjectModel.Services.BindingConverters;
using Microsoft.Extensions.DependencyInjection;
using RefCasesServerExtension.BindingConverters;
using RefCasesServerExtension.Services;

namespace RefCasesServerExtension
{
    public class RefCasesServerExtension : WebClientExtension
    {
        public RefCasesServerExtension(IServiceProvider serviceProvider)
            : base()
        {
        }

        public override string ExtensionName
        {
            get { return Assembly.GetAssembly(typeof(RefCasesServerExtension)).GetName().Name; }
        }

        public override Version ExtensionVersion
        {
            get { return new Version(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion); }
        }

        #region WebClientExtension Overrides

        public override void InitializeServiceCollection(IServiceCollection services) (1)
        {
            services.AddSingleton<IBindingConverter, RefCasesConverter>(); (2)
            services.AddSingleton<IRefCasesService, RefCasesService>(); (3)
        }

        protected override List<ResourceManager> GetLayoutExtensionResourceManagers() (4)
        {
            return new List<ResourceManager>
            {
                { Resources.ResourceManager}
            };
        }

        #endregion
    }
}
1 Регистрация компонентов, реализованных в расширении.
2 Регистрируем конвертер.
3 Регистрируем сервисы
4 Также для примера добавлено несколько локализованных текстовых ресурсов, которые будут использоваться в клиентском расширении.
Контроллеры регистрировать не требуется.