Регистрация | Забыли пароль
Новости
| Новости | Фото дня | Интервью | Блог | Дайджест | Форум |
| Почта | Поиск | Работа | Бизнес-сеть | IT-календарь | Wi-Fi в Украине | Мануалы | Отправка SMS |
| Супермаркет | Компьютеры | Книги | Мелодии и игры |
e-mail Пароль запомнить меня
Опции сайта Главная Журналы Новости Сеть пользователей Объявления Рейтинг
Новый пользователь
Последние записи из журналов
Журналы пользователя
Связаться
Максим Сохацкий
Software Engineer в International Land Systems, Inc.

Бесплатный член сообщества с 29 мая. 2006 г.  (последний раз был в системе 14 янв. 2008 г.)
 
Информация о пользователе    Интервью   
Журналы
Максим Сохацкий имеет бизнес-контактов: 183

Рейтинг пользователя: 3 (3127 баллов)
Добавить в круг общения

Подписаться
Построение редакторов для объектов .NET из журнала пользователя Информационные системы
(Разместил Максим Сохацкий 22 июн. 2006 г)

 

Уровень читателя
Эта статья предполагает знание C# и архитектуры связывания в .NET Framework. В статье затрагиваются вопросы масштабируемости на платформу .NET Compact Framework.
Аннотация
Эта статья рассказывает о том как можно создать эффективную систему построения редакторов для любых объектов .NET Framework. Очень часто бывает, что нужно создать форму для редактирования бизнес объекта автоматически. Именной этой задаче и посвящена данная статья. В статье раскрывается один из подходов к реализации построителей редакторов, рассматривается как создавать формы, элементы управления которых автоматически связанны с членами-свойствами бизнес объекта. Также затронуты вопросы масштабируемости на платформу .NET Compact Framework.
Исходный код
Исходный код цитируемый в статье можно получить здесь: (после резолюции и договора на публикацию статью). Он включает в себя базовую систему построения редакторов для объектов .NET и компоненты для регистрации своих собственных элементов управления для необходимых типов прикладной области использования, а так же поддерживает платформу .NET Compact Framework.
Введение
Любая система начинается с того, что нужно построить механизм отображения какого-либо бизнес объекта, т.е. разработать механизм ввода, отображения и редактирования бизнес объектов. Для систем начального уровня допустимо использование разовых, специально созданных форм, однако для больших систем, а также для систем, где недетерминирован формат бизнес объекта, необходим более гибкий механизм.
Цели, поставленные перед нами, можно сформулировать следующим образом. 
  • Редактор любого объекта должен представляется в виде дерева элементов управления или подредакторов.
  • Для определенных типов свойств объекта (System.Type) надо уметь регистрировать элементы управления (Control).
  • Редактор должен строится как для любых объектов CLR посредством Reflection, так и по ICustomTypeDescriptor объекта который поддерживает связывание (Binding).
  • Нужно выделить механизм позиционирования элементов управления.
  • Позиционирование элементов управления «метка» возле элементов управления, которые представляют значения вынести в «компоновщики»
  • Реализовать механизм раскраски редакторов в соответствии с определенными атрибутами свойств объекта (например ключевые поля и т.д.)
  • енерировать редакторы которые автоматически поддерживают связывание.
Нами была предложена следующая модель организации системы построения редакторов. Система состоит из графа провайдеров, классов которые отвечают на определенные типы объектов запросов. Обычно для каждого типа запросов – свой провайдер. Например, мы разделили механизм построения редактора на такие элементарные провайдеры, задача каждого из которых, внести свою маленькую работу в общую цель построения редактора: 
  • Декомпозиция объекта на члены;
  • Создание элементов управления по типу данных свойств-членов;
  • Декораторы («раскраска» элементов управления), сюда входит:
    1. расстановка меток;
    2. связывание элементов управление с членами объекта.
  • Компоновщик, провайдер который расставляет подредакторы на панель или форму.
Самый верхний запрос включает в себя объект для которого нужно построить редактор. Провайдер который отвечает за этот запрос просматривает этот объект, делает его декомпозицию на члены и генерирует запросы для построения редакторов для свойств-членов обратно в систему. Далее по цепочки отвечают другие провайдеры и т.д. пока есть запросы и есть кому отвечать на них.
Провайдер
EditorProvider - это класс, который отвечает на EditorRequest и создает для него редактор. Каждый такой класс понимает определенный список запросов, который ему передается в конструктор. Главное его предназначение - это определить понимает ли он определенный запрос, и, если да, то отдать нужный редактор.
public interface IEditorProvider {
            bool IsSuitable(IServiceProvider sp, EditorRequest r);
            IEditor GetEditor(IServiceProvider sp, EditorRequest r);
}
Его реализация в себе хранит список типов которые он понимает, как показано на рис. 1.
Рис. 1. Провайдер хранит список запросов на которые он отвечает
 
     +------+    +---------------------+
     | PRO0 |----| EditorRequest       |
     +------+    +---------------------+
                 | MyEditorRequest     | -> returns new MyEditor()
                 +---------------------+
                 | FooEditorRequest    |
                 +---------------------+
                 | ...                 |
                 +---------------------+
 
Например если мы хотим что бы наш провайдер отвечал только на запрос типа MyEditorRequest и выдавал редакторы которые содержат элементы управления типа MyEditorControl реализуем интерфейс:
 
bool IsSuitable(IServiceProvider sp, EditorRequest r) {
            return typeof(EditorRquest).IsAssignableFrom(typeof(MyEditorRequest));
}
 IEditor GetEditor(IServiceProvider sp, EditorRequest r) {
            return new Editor(sp, "MyEditor", r, new MyEditorControl());
}
 
Диспетчер
EditorDispatcher - это набор провайдеров. Диспетчер - это абстрактный класс, к интерфейсу провайдера у него есть еще один метод, который определяет нужный провайдер для определенного запроса:
protected abstract IEditorProvider Dispatch(IServiceProvider sp, EditorRequest r, ref object state);
public abstract ICollection RegisteredProviders { get; }
 
Поскольку это тоже провайдер, то когда у него спросят редактор по запросу, он пробегает по своим провайдерам и у каждого спрашивает, может ли тот построить редактор для такого запроса. Если тот может, то возвращается редактор построенный найденным провайдером. Благодаря этому можно регистрировать диспетчеров в диспетчере и строить деревья провайдеров, как показано на рис. 2.
Рис. 2. Дерево провайдеров
     +------+  +------+
     | DSP0 |--| PRO0 |
     +------+  +------+  +------+
               | DSP1 |--| PRO5 |
               +------+  +------+  +------+
               | PRO1 |  | DSP2 |--| PRO6 |
               +------+  +------+  +------+
               | PRO2 |         
               +------+  +------+  +------+
               | DSP3 |--| DSP4 |--| PRO9 |
               +------+  +------+  +------+
               | PRO4 |  | PRO7 |  | PROA |
               +------+  +------+  +------+
                         | PRO8 |         
                         +------+         
}
                             
Для пользования этим например можно создать следующий класс в котором можно регистрировать провайдеров. Этот клласс, получив запрос, передает его всем провайдерам которые в нем зарегистрированы.
public class SimpleDispatcher : EditorDispatcher { }
Стек провайдеров
Вернемся к нашей задаче. Она состоит в том, что бы для любого объекта в системе который поддерживает или не поддерживает связывание, можно было построить форму. В случае если объект не поддерживает связывание, элементы управления должны строится по свойствам объекта которые берутся из Reflection. В другом случае - если объект поддерживает связывание, элементы управления должны строится по описателям PropertyDescriptor. И в том и в другом случае мы хотим, что бы можно было регистрировать определенные типы редакторов (обобщенных элементов управления) для определенные типов свойств объектов. 
·  Строить автоматически редактор для любого объекта
·  Получать список полей как из Reflection так из описания системы связывания
·  Настраивать редактор для любого типа данных
При описании провайдеров мы будем показывать реализацию главной функции где сосредоточена логика построения редактора. Из кода умышленно исключены обработка ошибок и проверки на null для чистоты понимания.
ObjectEditorProvider (OEP)
Самый главный запрос в нашу систему назовем ObjectEditorRequest (OER). Он описывает какой тип редактора мы хотим получить на выходе (например получить редактор в панели (Control, Panel) или получить сразу окно (Form) для редактирования объекта, т.е. более функциональный редактор) а также сам объект который может поддерживать связывание. Вот как, например, OEP реагирует на запрос:
 
protected override IEditor ProvideEditor(IServiceProvider sp, EditorRequest r) {
ObjectEditorRequest er = (ObjectEditorRequest)r;
IEditorContainer editor = null;
… // create editor …
foreach (PropertyDescriptor pd in er.Properties) {
                        IEditor sub = Editor.Create(sp,
new PropertyEditorRequest(editor.NestedControlsType, pd, er.DataSource));
                        if (!sub.IsEmpty) editor.Subeditors.Add(sub);
}
return editor;
}
 
Т.е. OEP делает декомпозицию объекта на его свойства и для каждого из них делает запрос обратно в систему с помощью статического метода редактора Editor.Create:
 
public static IEditor Create(IServiceProvider sp, EditorRequest r) {
            IEditorProvider ep = (IEditorProvider)sp.GetService(typeof(IEditorProvider));
            return ep.GetEditor(sp, r);
}
 
OER возвращает Properties (список полей) которые он берет у Reflection либо, если объект поддерживает связывание у дескриптора ICustomTypeDescriptor.
 
public PropertyDescriptorCollection Properties {
            get {
                        if (properties == null) {
ICustomTypeDescriptor td = this.DataSource as ICustomTypeDescriptor;
properties = (td == null) ? TypeDescriptor.GetProperties(dataSource) :
td.GetProperties();
                        }
                        return properties;
            }
            set { properties = value; }
}
FormsControlProvider (FCP)
 
Этот провайдер создает контейнер для элементов управления. CategoryContainer – это наш элемент управления (панель которая «сворачивается») для категорий. Этот элемент управления можно посмотреть на рисунках 1, 2 и 5.
 if (r.EditorType == typeof(ScrollableControl))
                        r.EditorType = r.SubContainer ? typeof(CategoryContainer) : typeof(Panel);
IEditor editor = ProvideEditor(sp, new ControlRequest(r.EditorType));
ControlProvider (CP)
 
CP создает элементы управления по запрашиваемому типу данных:
 
protected override IEditor ProvideEditor( IServiceProvider sp, EditorRequest r) {
            ControlRequest cr = (ControlRequest)r;
            object control = Activator.CreateInstance(cr.EditorType);
            return new Editor(sp, cr.EditorType.Name, r, control);
}
ControlMapper (CM)
 
Этот провайдер хранит хэш-таблицу, где типы данных (int, string) – ключи, а значения - типы элементов управления (TextBox, DataGrid). В нем есть два метода Register и Unregister. По умолчанию его можно заполнить, например, так:
 
Register(typeof(bool), typeof(CheckBox));
Register(typeof(string), typeof(TextBox));
Register(typeof(DateTime), typeof(DateEditor));
Register(typeof(int), typeof(TextBox));
Register(typeof(System.Drawing.Color), typeof(TextBox));
Register(typeof(IList), typeof(DataGrid));
 
Этот провайдер реагирует на DataEditorRequest и создает соответствующий элемент управления. Используется этот провайдер при создании редакторов для объектов СLR. Для объектов поддерживающих связывание он тоже годится но обычно бизнес объекты состоят не из типов CTS а из их производных и более приближенных к некоторой предметной области например: Stock, DocumentIdentiry, Order, DetailRow и т.д. Диспетчеризации по типу там будет не достаточно там нужны будут еще и атрибуты, например ключевое это поле или нет и т.д.
 
protected override IEditor ProvideEditor(IServiceProvider sp, EditorRequest r) {
DataEditorRequest er = (DataEditorRequest)r;
            Type controlType = (Type)mappings[er.DataType];
            foreach (Type t in mappings.Keys)
                        if (t.IsAssignableFrom(er.DataType)) {
                                   controlType = (Type)mappings[t];
                                   break;
                        }
            return (controlType != null)
                        ? Editor.Create(sp, new ControlRequest(controlType), r)
                        : Editor.Empty;
}
PropertyEditorProvider (PEP)
 
PEP реагирует на PER полученный от OEP и преобразовывает его в DER который нужен CM для создания конечного элемента управления.
Декораторы
 
Декораторы – это такие провайдеры которые кроме того что возвращают декорированные редакторы, например нужно для редактора проставить метки (Labels) на основании информации из PropertyDescriptor, покрасить их в нужный цвет и т.д. Декораторы поддерживают еще такую спецификацию:
 
protected virtual bool IsDecoratorSuitable(IServiceProvider sp, IEditor e) { return true; }
protected abstract IEditor Decorate(IEditor e);
 
public IEditor GetEditor(IServiceProvider sp, EditorRequest r) {
            DecoratorRequest dr = r as DecoratorRequest;
            return (IsDecoratorSuitable(sp, dr.Editor)) ? Decorate(dr.Editor) : dr.Editor;
}
PropertyEditorNameDecorator (PEND)
 
PEND декоратор реагирует на PER который генерирует OEP. На него так же отвечает PEP.
 
protected override IEditor Decorate(IEditor e) {
            PropertyEditorRequest er = e.Request as PropertyEditorRequest;
            e.Name = er.Descriptor.Name;
            e.ExtendedProperties["DisplayName"] = er.Descriptor.DisplayName;
            return e;
}
FormsEditorBinder (FEB)
 
Этот декоратор отвечает за связывание генерируемых элементов управления с свойствами-членами нашего объекта для которого нужно построить форму. Он декорирует, производя связывание полей объекта на определенные поля элементов управления. Для этого он хранит в себе список того, что нужно связывать с чем, например:
 
Register(typeof(DateEditor), "Date");
Register(typeof(DataGrid), new EditorBinder(BindDataGrid));
Register(typeof(Control), "Text");
 
Как видите в правилах можно указывать правила для связывания сложных элементов управления (обычно это те которые связываются с объектами IList). Например в случае DataGrid можно связывать не с помощью DataBindings.Add а напрямую присвоив DataSource члену объекта DataGrid составное свойство-член нашего объекта. Часто в этом случае нужно выполнять дополнительную настройку элемента управления, именно для этого мы ввели возможность регистрации делегата связывания.
 
protected override IEditor Decorate(IEditor editor) {
            PropertyEditorRequest er = editor.Request as PropertyEditorRequest;
            Control c = (Control)editor.Control;
            Type controlType = TypeDispatcher.FindAssignable(rules, c.GetType());
            object rule = rules[controlType];
BindingClosure bc = new BindingClosure(editor, rule);
            return editor;
}
 
void BindDataGrid(IEditor editor) {
PropertyEditorRequest r1 = (PropertyEditorRequest)editor.Request;
            DataGrid c = (DataGrid)editor.Control;
            c.DataSource = r1.Descriptor.GetValue(r1.DataSource);
}
 
Для того что бы динамически связывать элементы управления с членами объекта по строкам или вызывая правила, надо это хранить в каком-то контексте, для этого мы написали незамысловатый класс который связывает редактор с правилом связывания. Не надо забывать что связывать надо уметь динамически по возникновению события ParentChanged.
 
public class BindingClosure {
 
IEditor editor;
object rule;
EventHandler handler;
 
public BindingClosure(IEditor editor, object rule) {
                        this.editor = editor;
                        this.rule = rule;
                        this.handler = new EventHandler(UpdateBinding);
((Control)editor.Control).ParentChanged += handler; }
 
public void UpdateBinding(object sender, EventArgs e) {
                        Control c = (Control)sender; 
                        c.Invoke(new EventHandler(CreateBinding)); }
 
void CreateBinding(object sender, EventArgs e) {
PropertyEditorRequest er = editor.Request as PropertyEditorRequest;
                        if (rule is string)
                                   SimpleBindControl(editor, er, (string)rule);
                        else if (rule is EditorBinder)
                                   ((EditorBinder)rule)(editor); }
 
void SimpleBindControl(IEditor editor, PropertyEditorRequest er, string propertyName) {
                        Control c = (System.Windows.Forms.Control)editor.Control;
                        c.DataBindings.Add(propertyName, er.DataSource, er.Descriptor.Name); }
 
}
FormsEditorComposer (FEC)
Компоновщик FEC – это декоратор в котором сосредоточена логика расстановки элементов управления на форме. Вот например как может выглядеть композер для .NET CF.
protected override IEditor Decorate(IEditor r) {
            Type controlType = r.Control.GetType();
            Control container = (Control)r.Control;
            ScrollableControl sc = container as ScrollableControl;
            foreach (IEditor editor in r.Subeditors)
                        Compose(sc, editor, ref baseLine);
            baseLine = 0;
            foreach (Control c in sc.Controls)
if (baseLine < c.Bottom) baseLine = c.Bottom;
}
protected void Compose(ScrollableControl container, IEditor e, ref int baseLine) {
            if (e.IsEmpty) return;
            Control c = (Control)e.Decorated;
            Label label = null;
            IEditor labelEditor = Editor.Create(e.ServiceProvider, new ControlRequest(typeof(Label)));
            label = (Label)labelEditor.Decorated;
            label.Top = baseLine;
            label.Text = ILSR.GetString("{0}:",
                        e.ExtendedProperties.Contains("DisplayName") ?
                        (string)e.ExtendedProperties["DisplayName"] : e.Name);
            container.Controls.Add(label);
            container.Controls.Add(c);
            c.Top = baseLine + 20;
            c.Left = 4;
            baseLine = c.Bottom + 4;
            container.Height = baseLine;
}
Достоинства этой модели – гибкость, потому как все части системы открыты и система общается между собой через тот же механизм как пользователь с системой, через запросы. Можно дописывать свои провайдеры которые будут реагировать на новые запросы, регистрировать их, таким образом расширяя систему. Для простого общепринятого табличного расположения контрольных элементов система отвечает всем требованиям.
Недостатки. В некоторых случая бывает нужно создавать формы вручную, так как автоматически построенные редакторы выглядят несколько механично. Зачастую чтобы реализовать некоторый нетривиальный алгоритм построения надо потратить время на расширения системы больше чем спроектировать формы вручную.
Как этим пользоваться
Создайте C# WindowsApplication. Подключите к проекту ссылку на ILS.UI.dll. Положите на форму кнопку и панель. Допишите в код, сгенерированный студией следующие строки:
using ILS.UI;
// Добавьте член формы
private ServiceContainer sc = new ServiceContainer();
// Добавьте обработчик события для формы
private void Form1_Load(object sender, System.EventArgs e) {
      SimpleDispatcher dispatcher = new SimpleDispatcher();
      FormsControlProvider fcp = new FormsControlProvider(true);
      FormsEditorComposer fec = new FormsEditorComposer(fcp, true);
      Data2FormsControlMapper d2f = new Data2FormsControlMapper();
      ObjectEditorProvider oep = new ObjectEditorProvider();
       PropertyEditorProvider pep = new PropertyEditorProvider();
      PropertyEditorNameDecorator pend = new PropertyEditorNameDecorator(pep, false);
      FormsEditorBinder feb = new FormsEditorBinder(pend, true);
       dispatcher.Register(d2f);
      dispatcher.Register(oep);
      dispatcher.Register(fec);
      dispatcher.Register(feb);
      sc.AddService(typeof(IEditorProvider), dispatcher);
 }
// Добавтье обработчик кнопки
private void button1_Click(object sender, System.EventArgs e) {
      CreateEditor(button1, true);
}
 
// Допишите функцию
void CreateEditor(object obj, bool cat) {
      panel1.Controls.Clear();
      ObjectEditorRequest oer = new ObjectEditorRequest(typeof(Control), obj);
      oer.Categorized = cat;
      IEditor editor = Editor.Create(sc, oer);
      c = (Control)editor.Decorated;
      c.Dock = DockStyle.Fill;
      panel1.Controls.Add(c);
}
На рис. 3 и рис. 4 показано как создается редактор для объекта кнопка (button1) которая положена на форму в левом верхнем углу. Благодаря связыванию мы можем во времени выполнения изменить высоту кнопки с помощью этого редактора и это сразу отобразится на форме.
Рис. 3 и 4. Редактор для кнопки до и после изменения значения поля объекта
 
 
Редактор поддерживает категории, особый атрибут PropertyDescriptor благодаря которому мы можем сгруппировать поля объекта, так как это делает стандартный PropertyGrid. Можно выключить категории. На рис. 6 показана генерация редактора для платформы .NET CompactFramework с использованием другого компоновщик (который «метки» ставит не слева контрольных элементов, а сверху) без использования категорий (их нет у PropertyDescriptor на .NET Compact Framework).
Рис. 5. Редактор без категорий                  Рис 6. Редактор для .NET Compact Framework
 
 
На рис. 7 показан редактор для бизнес объекта IDocument (который используется в системе DSS компании ILS), поддерживающего связывание с элементами управления для определенных типов свойств бизнес объекта. Два последних поля – это типизированные IList свойства первый из которых содержит картинки, а второй - массив объектов IDocument.
Рис. 7. Редактор для бизнес объектов IDocument используемых в компании ILS.
 
Заключение
Как видите система обладает достаточной гибкостью для построения редакторов для любого рода объектов в .NET Framework. Обратите внимание на рисунки 4 и 5, здесь используется единый код для построения редактора, единая система провайдеров и диспетчеров, но по разному настроенные компоновщики, и разный набор зарегистрированных элементов управления.
В будущем можно расширить и дополнить систему по таким направлениям. Во первых благодаря тому что ассоциации типов данных к элементами управления сосредоточены в одном провайдере, можно заменить его для создания редакторов для ASP.NET. Во вторых можно дополнить текущий компоновщик, который расставляет контрольные элементы в столбик, более сложными реализациями, как то, например, табличным компоновщиком или более сложным механизмом расстановки элементов управления.
Об авторе
Вопросы и комментарии присылайте по адресу mes@ils.com.ua.
Максим Э. Сохацкий, магистр прикладной математики, архитектор отдела информационных систем ИЛС-Украина. Сейчас работает над книгой «Построение систем управление бизнес процессами».
 
Статья написана специально для Atlaskit
 


Ключевые слова: .net

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


c ITUA.info: Information Technologies of Ukraine
Информационные технологии: последние новости.
При полном или частичном использовании материалов сайта
гиперссылка на http://itua.info обязательна.
Реклама на сайте - IMA UaMaster.

Rambler's Top100