Создание описателя элемента управления и расширения программы Конструктор Web-разметок

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

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

  • Свойства для выбора источника данных.

  • Специальное свойство для ограничения области выбора дел.

  • Стандартный набор событий (при наведении/отведении курсора и при смене данных).

Описатель элемента управления

Для описания данного элемента управления сформируем текстовый описатель со следующим содержимым:

Файл RefCategoriesDesignerExtension\xml\RefCasesControlDescription.xml
<?xml version="1.0" encoding="utf-8" ?>
<Controls>
  <Control Name="RefCases" ControlGroupResourceKey="ControlGroup_Directories"  ResourceKey="Control_RefCases">
    <Properties>
    (1)
      <Property Type="Name" /> (2)
      <Property Type="PlaceHolder" /> (3)
      <Property Type="Tip" /> (4)
      <Property Type="LabelText" />
      <Property Type="ShowEmptyLabel" />
      <Property Type="Visibility" />
      <Property Type="StandardCssClass" DefaultValue="ref-cases" />
      <Property Type="CustomCssClasses" />
      <Property Type="TabStop" DefaultValue="True" />
      <Property Type="Required" />

    (5)
      <Property Type="ExtendedDataSource" /> (6)
      <Property Type="DataSource" /> (7)
      <Property Type="DataField" Editor="RefCasesFieldMetadataEditor" />
      <Property Type="Binding" BindingConverter="RefCasesConverter" /> (8)
      <Property Type="EditOperation" /> (9)

      <Property Name="RootSection" ResourceKey="Control_RefCases_Section"
                Category="BehaviorCategory" DataType="System.Guid"
                Editor="RefCasesSectionEditor" /> (10)
    </Properties>
    <Events> (11)
      <Event Name="Click" ResourceKey="ControlTypes_ClickEventProperty" />
      <Event Name="MouseOver" ResourceKey="ControlTypes_MouseOverEventProperty" /> (12)
      <Event Name="MouseOut" ResourceKey="ControlTypes_MouseOutEventProperty" /> (13)
      <Event Name="DataChanged" ResourceKey="ControlTypes_DataChangedEventProperty" /> (14)
    </Events>
  </Control>
</Controls>
1 Стандартный набор свойств:
2 Название;
3 Заполнитель;
4 Подсказка.
5 Свойства для настройки источника данных и операции редактирования:
6 ExtendedDataSource — выбор расширенного источника данных;
7 DataSource, DataField — выбор секции и поля с данными элемента управления. Редактор RefCasesFieldMetadataEditor (будет разработан далее) реализует заявленную функциональность ограничения полей карточки, доступных для выбора;
8 Binding — системное свойство, с которым на клиент передаётся связь с источником данных.

Значение элемента управления — идентификатор строки дела, выбранного в Справочнике номенклатуры дел. Пользователю предпочтительно показывать название дела из справочника, а не идентификатор.

Такое преобразование данных осуществляется с помощью конвертера, название которого указывается в атрибуте BindingConverter. В данном примере название конвертера RefCasesConverter (будет разработан далее);

9 EditOperation — выбор операции редактирования.
10 Новое свойство RootSection обеспечивает заявленную возможность выбора разделе Справочника номенклатуры дел, из которого пользователю будет разрешено выбирать дела.

Значение свойства будет устанавливаться с помощью специального редактора RefCasesSectionEditor (будет разработан далее в серверном расширении Web-клиента).

11 Также для элемента управления определены события:
12 При наведении курсора;
13 При отведении курсора;
14 При смене данных.

Расширение программы Конструктор Web-разметок

В описателе элемента управления определено несколько нестандартных редакторов, а также ресурсы с текстовыми значениями. Данные сущности нужно добавить в Конструктор Web-разметок с помощью расширения, код которого приведён ниже.

Файл RefCategoriesDesignerExtension\Extension\RefCasesDesignerExtension.cs
using DocsVision.Platform.Tools.LayoutEditor.Extensibility;
using RefCasesDesignerExtension.Editors;
using System;
using System.Collections.Generic;
using System.Resources;

(1)
namespace RefCasesDesignerExtension.Extension
{
    class RefCasesDesignerExtension : WebLayoutsDesignerExtension
    {
        public RefCasesDesignerExtension(IServiceProvider provider)
            : base(provider)
        {
        }

        protected override Dictionary<string, Type> GetEditors()
        {
            return new Dictionary<string, Type>
            {
                { "RefCasesFieldMetadataEditor", typeof(RefCasesFieldMetadataEditor)}, (2)
                { "RefCasesSectionEditor", typeof(RefCasesSectionEditor)} (3)
            };
        }

        protected override List<ResourceManager> GetResourceManagers()
        {
            return new List<ResourceManager>
            {
                Resources.ResourceManager
            };
        }
    }
}
1 В данном расширении регистрируется два редактора, которые указаны в текстовом описателе:
2 RefCasesFieldMetadataEditor — редактор для выбора поля карточки, содержащего значение элемента управления;
3 RefCasesSectionEditor — редактор для выбора раздела Справочника номенклатуры дел, из которого пользователям будет разрешено выбирать дела.

Редактор "RefCasesFieldMetadataEditor"

Редактор RefCasesFieldMetadataEditor является стандартным редактором редактором для выбора поля карточки, реализованном в классе Docsvision.BackOffice.WebLayoutsDesigner.Editors.FieldMetadataEditor.

Файл RefCategoriesDesignerExtension\Editors\RefCasesFieldMetadataEditor.cs
using System.Windows;
using DocsVision.Platform.Data.Metadata.CardModel;
using System;
using Xceed.Wpf.Toolkit.PropertyGrid;
using Xceed.Wpf.Toolkit.PropertyGrid.Editors;
using DocsVision.BackOffice.WebLayoutsDesigner.Editors;
using DocsVision.Platform.WebLayoutsDesigner.NewEditors;

namespace RefCasesDesignerExtension.Editors
{
    public class RefCasesFieldMetadataEditor : ITypeEditor (1)
    {
        public FrameworkElement ResolveEditor(PropertyItem propertyItem)
        {
            var refCasesID = new Guid("246197EA-846A-44DA-9EA3-0BCAE5500388");
            var sectionCasesID = new Guid("56AF8231-B918-42D4-AC15-90EC2E9A0725");

            var editor = new FieldMetadataEditor();

            editor.FieldFilter = (field) => { (2)
                return (field.FieldType == FieldType.RefId
                        && field.LinkedCardTypeId == refCasesID
                        && field.LinkedSectionId == sectionCasesID);
            };

            return editor.ResolveEditor(propertyItem);
        }

    }
}
1 Редактор для выбора поля карточки, ссылающегося на Дело в Справочнике номенклатуры дел.
2 Устанавливаем фильтр для выбора полей только из справочника.

Ограничение возможности выбора полей карточки включено с помощью фильтра FieldFilter, в котором проверяется тип поля (field.FieldType), которое должно быть ссылочным полем (FieldType.RefId), ссылающимся на секцию Дела (field.LinkedSectionId == sectionCasesID) Справочника номенклатуры дел (field.LinkedCardTypeId == refCasesID).

В стандартной реализации приложения Делопроизводство 5 поле карточки, используемое для хранения ссылки на Дело, не является ссылочным, поэтому для него не подходит фильтр, использованный в данном примере, при настройке разметки данное поле будет недоступно для выбора. Если в Web-клиенте нужно повторить настройки разметки Windows-клиента, фильтр нужно изменить следующим образом:

var editor = new FieldMetadataEditor();

editor.FieldFilter = (field) => {
    return (field.FieldType == FieldType.RefId
        && field.LinkedCardTypeId == refCasesID
        && field.LinkedSectionId == sectionCasesID);
};

Редактор "RefCasesSectionEditor"

Редактор RefCasesSectionEditor имеет более сложную реализацию (по сравнению с RefCasesFieldMetadataEditor), из-за необходимости отображения дерева Разделов Справочника номенклатуры дел 5.

У данного редактора есть две составляющие:
  • Графическая — предоставляет форму для выбора Разделов.

  • Функциональная — предоставляет функции, загружающие дерево Разделов и выполняющие сопутствующие операции.

В простейшем случае можно обойтись без сложного редактора, и предоставить возможность непосредственно вводить идентификатор требуемого Раздела справочника в значение настройки элемента управления, или отказаться от данной настройки.

Графическая и функциональные составляющие также распределяются между двумя компонентами:

  • Основной компонент редактора с реализацией интерфейса ITypeEditor.

  • Форма с деревом Разделов.

Далее приведён код основного компонента редактора без графической составляющей (см. полный исходный код примера на GitHub).

using DocsVision.Platform.Tools.LayoutEditor;
using DocsVision.Platform.Tools.LayoutEditor.PropertiesEditor;
using DocsVision.Platform.WebClient;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using Xceed.Wpf.Toolkit.PropertyGrid;
using Xceed.Wpf.Toolkit.PropertyGrid.Editors;

namespace RefCasesDesignerExtension.Editors
{
    public partial class RefCasesSectionEditor : UserControl, ITypeEditor
    {
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(Guid), typeof(RefCasesSectionEditor),(1)
        new FrameworkPropertyMetadata(Guid.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
        public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(RefCasesSectionEditor), new FrameworkPropertyMetadata(string.Empty));

        public Guid Value (2)
        {
            get { return (Guid)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public string Text (3)
        {
            get { return (string)GetValue(TextProperty); }
            set {
                SetValue(TextProperty, value);
                Clear.Visibility = value != ""? Visibility.Visible: Visibility.Collapsed; // Кнопка очистки значения (4)
            }
        }

        private IServiceProvider serviceProvider;
        private SessionContext sessionContext;

        public RefCasesSectionEditor()
        {
            InitializeComponent();
        }

        public FrameworkElement ResolveEditor(PropertyItem propertyItem) (5)
        {
            var bindingObject = (IControlPropertiesObject)propertyItem.Instance;

            this.serviceProvider = bindingObject.ServiceProvider; (6)
            var currentObjectContextProvider = this.serviceProvider.GetService(typeof(ICurrentObjectContextProvider)) as ICurrentObjectContextProvider;
            this.sessionContext = currentObjectContextProvider.GetOrCreateCurrentSessionContext();

            Binding binding = new Binding("Value"); (7)
            binding.Source = propertyItem;
            binding.Mode = propertyItem.IsReadOnly ? BindingMode.OneWay : BindingMode.TwoWay;
            BindingOperations.SetBinding(this, RefCasesSectionEditor.ValueProperty, binding);

            if (this.Value != Guid.Empty) (8)
                this.Text = new RefCasesUtils(sessionContext).GetSectionTitle(this.Value);

            return this;
        }

        private void ShowSections_Click(object sender, RoutedEventArgs e) (9)
        {
            var sectionTree = new SectionsTree(sessionContext);
            if (sectionTree.ShowDialog() == true)
            {
                this.Value = sectionTree.SelectedNodeID;
                this.Text = sectionTree.SelectedNodeText;
            }
        }

        private void Clear_Click(object sender, RoutedEventArgs e) (10)
        {
            this.Value = Guid.Empty;
            this.Text = "";
        }
    }
}
1 Объявляем свойства зависимости для связывания со значением настройки (идентификатор Раздела) и отображаемым значением.
2 Идентификатор выбранного Раздела справочника — является значение настройки.
3 Название выбранного Раздела справочника, отображаемое в строке настройки.
4 Кнопка очистки значения.
5 Реализация метода ITypeEditor.ResolveEditor.
6 Получаем поставщика сервисов из элемента управления.
7 Связываем значение компонента с ValueProperty.
8 Получаем отображаемое значение выбранного Раздела при загрузке элемента.
9 Открываем форму для выбора Раздела.
10 Очищаем значение настройки.

Основные функции прокомментированы в коде. При реализации нового редактора (в данном случае не используются готовые реализации, как в редакторе RefCasesFieldMetadataEditor) особое внимание следует обратить на необходимость связывания значение настройки — Value — со свойством зависимости (в данном примере — ValueProperty).

Метод ShowSections_Click вызывается при нажатии кнопки выбора значения свойства. Данный метод открывает форму SectionsTree с деревом Разделов Справочника номенклатуры дел 5 (будет разработан далее).

Метод Clear_Click вызывается при нажатии кнопки очистки значения настройки.

Код вспомогательного метода RefCasesUtils.GetSectionTitle, возвращающего текстовое описание для Раздела справочника, идентификатор которого передан в метод:

Файл RefCategoriesDesignerExtension\Editors\RefCasesUtils.cs
public string GetSectionTitle(Guid sectionId)
{
    SectionData sectionSesction = cardManager.GetDictionaryData(refCasesId).Sections[sectionsSectionID]; (1) (2) (3)

    if (sectionSesction.RowExists(sectionId) == false) (4)
        return "Ошибка!";

    RowData sectionRow = sectionSesction.GetRow(sectionId);
    RowData yearRow = sectionRow.SubSection.ParentRow;

    if (yearRow != null)
        return string.Format("{0}. {1}", yearRow["Year"], sectionRow["Name"].ToString());

    return "Ошибка!";
}
1 cardManager — Менеджер карточек (базовое API Docsvision).
2 refCasesId — идентификатор Справочника номенклатуры дел 5.
3 Возможно раздел был удалён.
4 sectionsSectionID — идентификатор секции "Разделы" справочника.

Далее приведён код компонента дерева Разделов без графической составляющей (см. полный исходный код примера на GitHub).

Файл RefCategoriesDesignerExtension\Editors\SectionsTree.xaml.cs
using DocsVision.Platform.WebClient;
using System;
using System.Windows;

namespace RefCasesDesignerExtension.Editors
{

    public partial class SectionsTree : Window
    {
        public string SelectedNodeText = "";
        public Guid SelectedNodeID = Guid.Empty;
        private RefCasesUtils refCasesUtils;

        public SectionsTree(SessionContext sessionContext)
        { (1)
            InitializeComponent();
            refCasesUtils = new RefCasesUtils(sessionContext);

            Years.ItemsSource = refCasesUtils.GetYears(); (2)
        }

        private void Years_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) (3)
        {
            if (Years.SelectedIndex == -1 || !(Years.SelectedItem is Year))
                return;

            Year selectedYear = Years.SelectedItem as Year;

            Sections.ItemsSource = refCasesUtils.GetSections(selectedYear.ID); (4)
        }

        private void Accept_Click(object sender, RoutedEventArgs e) (5)
        {
            if (Sections.SelectedItem == null)
                return;
            var selectedNode = Sections.SelectedItem as Node;
            var selectedYear = Years.SelectedItem as Year;

            this.SelectedNodeText = string.Format("{0}. {1}", selectedYear.Value, selectedNode.Name);
            this.SelectedNodeID = selectedNode.ID;

            this.DialogResult = true;
            this.Close();
        }

        private void Cancel_Click(object sender, RoutedEventArgs e) (6)
        {
            this.Close();
        }
    }
}
1 Для формирования дерева Разделов нужно получить данные из Справочника номенклатуры дел 5. Для этого объявляем необходимость передачи контекста сессии в конструкторе класса.
2 Получаем список лет из Справочника номенклатуры дел 5.
3 При выборе Года формируем дерево Разделов для данного года.
4 Получаем список Разделов из Справочника номенклатуры дел 5.
5 Обработка нажатия кнопки сохранения выбора.
6 Обработка нажатия отмены.

Основная "работа" здесь выполняется методами RefCasesUtils.GetYears, RefCasesUtils.GetSections.

Метод RefCasesUtils.GetYears получает все строки из секции Года Справочника номенклатуры дел 5:

Файл RefCategoriesDesignerExtension\Editors\RefCasesUtils.cs
public IEnumerable<Year> GetYears()
{
    refCasesData = cardManager.GetDictionaryData(refCasesId);
    SectionData yearSection = refCasesData.Sections[yearsSectionId];

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

Метод RefCasesUtils.GetSections получает дерево строк для секции Разделы Справочника номенклатуры дел 5.

Файл RefCategoriesDesignerExtension\Editors\RefCasesUtils.cs
public List<Node> GetSections(Guid yearID)
{
    var yearSection = refCasesData.Sections[yearsSectionId];

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

        return GetNodesFromRows(sectionRows);
    }

    return new List<Node>();
}

List<Node> GetNodesFromRows(RowDataCollection rows) (1)
{
    var nodes = new List<Node>();

    foreach (var row in rows)
    {
        var node = new Node() { ID = row.Id, Name = row["Name"].ToString() };
        if (row.HasChildRows)
            node.Nodes = GetNodesFromRows(row.ChildRows);

        nodes.Add(node);
    }
    return nodes;
}
  1. Возвращает список Разделов для строк секции справочника