Построение редакторов для объектов .NET Максим Сохацкий Отдел информационных систем ИЛС-Украина Аннотация Любая система начинается с того, что нужно построить механизм отображения какого-либо бизнесс-объекта, т.е. разработать механизм ввода, отображения и редактирования бизнессобъектов. Для систем начального уровня допустимо использование разовых, специально созданных форм, однако для больших систем, а также для систем, где недетерминирован формат бизнесс-объекта, необходим более гибкий механизм. Цели, поставленные перед нами, можно сформулировать следующим образом. Редактор любого объекта должен представлятся в виде дерева элементов управления или подредактров. Для определенных типов свойств объекта (System.Type) надо уметь регистрировать элементы управления (Сontrol). Редактор должен строится как для любых объектов CLR посредством Reflection, так и по ICustomTypeDescriptor объекта который поддерживает биндинг. Нужно выделить механизм позиционирования элементов управления. Позиционирование элементов управления «метка» возле элементов управления, которые представляют значения вынести в «композеры» Реализовать механизм раскраски редакторов в соответствии с определенными атрибутами ствойств объекта (например ключевые поля и т.д.) Генерировать редакторы которые автоматически поддерживают биндинг. Нами была предложена следующая модель организации системы построения редактров. Система состоит из графа провайдеров, классов которые отвечают на определенные типы объектов запросов. Обычно для каждого типа запросов – свой провайдер. Например, мы разделили механизм построения редактора на такие элементарные провайдеры, задача каждого из которых, внести свою маленькую работу в общую цель построения редактора: Декомпозиция объекта на члены; Генерирация элементы управления по типу; Раскраска элементов управления (Декорирование). Сюда входит: 1. расстановка меток; 2. биндинг. Провайдер который компонует все подредакторы на панель или форму (Композер). Самый верхний запрос включает в себя объект для которого нужно построить редактор. Провайдер который отвечает за этот запрос просматривает этот объект, делает его декомпозицию на члены и генерирует запросы для построения редакторов для свойствчленов обратно в систему. Далее по цепочки отвечают другие провайдеры и т.д. пока есть запросы и есть кому отвечать на них. Провайдер EditorPropvider - это такой класс, который отвечает на EditorRequest и создает для него редактор. Каждый такой класс понимает определенный список запросов, который ему передается в конструктор. Главное его предназначение - это определить понимает ли он определенный запрос, и, если да, то отдать нужный редактор. public interface IEditorProvider { bool IsSuitable(IServiceProvider sp, EditorRequest r); IEditor GetEditor(IServiceProvider sp, EditorRequest r); } Его реализация, естественно в себе хранит список типов которые он понимает: Диаграмма 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 +------+ +------+ | DSP0 |--| PRO0 | +------+ +------+ +------+ | DSP1 |--| PRO5 | +------+ +------+ +------+ | PRO1 | | DSP2 |--| PRO6 | +------+ +------+ +------+ | PRO2 | +------+ +------+ +------+ | DSP3 |--| DSP4 |--| PRO9 | +------+ +------+ +------+ | PRO4 | | PRO7 | | PROA | +------+ +------+ +------+ | PRO8 | +------+ Для пользования этим например можно создать следующий класс в котором можно регистрировать провайдеров public class SimpleDispatcher : EditorDispatcher { } Стек провайдеров Вернемся к нашей задаче. Она состоит в том, что бы для любого объекта в системе который поддерживает или не поддерживает биндинг можна было построить форму. В случае если обект не оддерживает биндинг контролы должны строится по пропертям объекта которые берутся из Reflection. В другом случае - если объект поддерживает биндинг, элементы управления должны строится по описателям PropertyDescriptor. И в том и в другом случае мы хотим, что бы можно было регистрировать определенные типы редакторов (обобщенных элементов управления) для определенные типов свойств объектов. Строить автоматически редактор для любого объкта Получать список полей как из рефлекшна так из описания биндинга Настраивать редактор для любого типа данных 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 либо, если объект поддерживает биндинг у проперти дескрипторов 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 создает контролы по System.Type: 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) Этот провайдер хранит хэштаблицу типов данных (System.Type) - ключи и значения типы контролов. В нем есть два метода 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) на основании информации из проперти десткрипора, покрасить их в нужный цвет и т.д. Декораторы поддерживают еще такую спецификацию: 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"); Как видите в правилах можно указывать правила для биндинга сложных контролов. Например в случае 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); } Для того что бы динамически биндить по строкам или вызывая правила, надо это хранить в каком-то контексте, для этого мы написали незамысловатый класс который связывает редактор с правилом биндинга, биндинг производится динамически. 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); } Рисунки 1 и 2 Сдесь показано как создается редактор для объекта кнопка (button1) которая положена на форму в левом верхнем углу. Благодаря биндингу мы можем в Run-Time изменить высоту кнопки с помощью этого редактора и это сразу отобразится на форме. Рисунки 3 и 4 Редактор поддерживает категории, особый аттрибут PropertyDescriptor благодаря которому мы можем сгруппировать поля объекта, так как это делает стандартный PropertyGrid. Можно выключить категории. На рисунке 4 показана генерация редактора для платформы .NET CF с использованием другого композера (который лейблы ставит не слева контролов, а сверху) без использования категорий (их нет у PropertyDescriptor на .NET CF). Рисунок 5 Вот как выглядит редактор для бизнес-объекта IDocument (который используется в системе DSS компании ILS), поддерживающего биндинг с настроеными контролами для определенных типов свойств бизнесс-объекта. Два последних поля – это типизированные IList свойства первый из которых содержит картинки, а второй массив обїектов IDocument. Как видите система обладает достаточной гибкостью для построения редакторов для любого рода объектов в .NET Framework. Обратите внимание на рисунки 4 и 5, здесь используется единый код для построения редактора, единая система провайдеров и диспетчеров, но по разному настроные композеры, и разный набор зарегистрированных контролов. -Максим Э. Сохацкий, ИЛС-Украина. [email protected]