Pull to refresh
0

Что скрывается за формой редактирования сложного объекта?

Reading time 9 min
Views 20K
В этой статье мы продолжаем знакомить вас с подходами, реализованными в планировщике XtraScheduler. В предыдущей статье мы рассказывали о синхронизаторе данных, на этот раз поговорим о формах.



Довольно часто в приложениях можно встретить формы, которые предназначены для ввода или редактирования объектов с большим количеством зависимых свойств. Построение таких форм ввода вызывает «головную боль» у разработчиков: рутинная работа по размещению редакторов, написание кода инициализации, валидации, обработчиков событий…

Так как же делать такие формы быстро и надежно?

Редактируемый объект


В качестве примера редактируемого объекта возьмем Appointment — объект, представляющий некое событие планировщика. Структура данного класса приведена на диаграмме ниже:



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

Поэтому при разработке интерфейса формы необходимо принять во внимание все эти факторы и сделать так, чтобы элементы интерфейса определённым образом реагировали на изменение состояния редактируемого объекта.

Способ редактирования


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

Вы уже наверное догадались, что речь пойдёт о контроллере и использовании паттерна Адаптер, так как интерфейс формы может отличаться от интерфейса самого объекта.

Создадим контроллер, передав ему в конструктор в качестве параметра исходный редактируемый объект, а также другие объекты, необходимые для выполнения редактирования.
public class AppointmentFormControllerBase : INotifyPropertyChanged {
    InnerSchedulerControl innerControl;
    Appointment sourceApt;
 
    public AppointmentFormControllerBase(InnerSchedulerControl innerControl, Appointment apt) {
        this.innerControl = innerControl;
    	this.sourceApt = apt;
        //...      
    }

    protected internal Appointment SourceAppointment { get { return sourceApt; } }
    //...
}

В нашем случае изменение исходного объекта в процессе редактирования было крайне нежелательно и при нажатии на кнопку Отмена в форме требовалось бы восстанавливать исходные значения всех его свойств. Поэтому наряду с исходным объектом, контроллер содержит также копию этого объекта, созданную в конструкторе:
public AppointmentFormControllerBase(InnerSchedulerControl innerControl, Appointment apt) {
    //...
    CreateAppointmentCopies();
}
public Appointment EditedAppointmentCopy { get { return editedAptCopy; } }
   	
protected internal virtual void CreateAppointmentCopies() {
    editedAptCopy = sourceApt.Copy();
   //...
}

Что даёт наличие копии? Пока осуществляется редактирование в форме, изменяются свойства объекта-копии и только при применении изменений свойства из копии «накатываются» на исходный объект. Это осуществляется специальным методом контроллера:
public virtual void ApplyChanges() {
    // …
    sourceApt.BeginUpdate();
    try {
        ApplyChangesCore();
    } finally {
        sourceApt.EndUpdate();
    }
}
protected internal virtual void ApplyChangesCore() {
    AppointmentFormAppointmentCopyHelper helper = new AppointmentFormAppointmentCopyHelper(this);
    helper.AssignSimpleProperties(editedAptCopy, sourceApt);
    helper.AssignCollectionProperties(editedAptCopy, sourceApt);
}

В случае закрытия формы без изменения необходимость каких либо действий отпадает, т.к. исходный объект остался нетронутым.

Заметим, что при другой архитектуре схема применения/отката изменений может отличаться от описанной тут и необходимость в копии может отсутствовать.

Интерфейс контроллера


Перейдём к свойствам. В контроллере реализуем паттерн Фасад и дублируем необходимые свойства из объекта-копии в свойства контроллера:
public string Subject {
    get { return editedAptCopy.Subject; }
    set { editedAptCopy.Subject = value; 
             NotifyPropertyChanged("Subject"); }
}

При несовместимости интерфейсов редакторов ввода и свойства объекта делаем необходимые преобразования прямо в свойствах контроллера:
public DateTime Start {
    get {  return InnerControl.TimeZoneHelper.ToClientTime(editedAptCopy.Start); }
    set {  editedAptCopy.Start = InnerControl.TimeZoneHelper.FromClientTime(value); }
}

При необходимости, например, раздельного показа и редактирования даты и времени, дробим свойство объекта на два свойства контроллера:
public DateTime StartDate {
    get { return editedAptCopy.Start.Date; }
    set { editedAptCopy.Start = value.Date + editedAptCopy.Start.TimeOfDay;                  
             NotifyPropertyChanged("StartDate"); }

}
public TimeSpan StartTime {
    get { return editedAptCopy.Start.TimeOfDay; }
    set { editedAptCopy.Start = editedAptCopy.Start.Date + value;
             NotifyPropertyChanged("StartTime"); }
}

Дополняем контроллер необходимыми служебными свойствами, которые отсутствуют в редактируемом объекте, но могут понадобиться при проектировании интерфейса формы:
public virtual bool IsNewAppointment { get { … } } 
public bool CanDeleteAppointment { get { … } } 


Кроме того, создаём в контроллере набор методов, реализующих всю необходимую логику, включая методы валидации, получения дополнительных объектов и т.д, которые будут запрашивать объекты формы.

Валидация осуществляется в обработчиках событий элементов управления формы и в свойства контроллера записываются уже верные значения. При изменении зависимых свойств в контроллере требуется обновить данные в соответствующих редакторах, а возможно даже скрыть или сделать недоступными для ввода.
В WinForms это делается следующим образом: отписываемся от событий редакторов, перечитываем новые данные из контроллера в редакторы, и снова подписываемся на события.
protected internal virtual void edtStartDate_Validated(object sender, EventArgs e) {
    controller.DisplayStart = edtStartDate.DateTime.Date + edtStartTime.Time.TimeOfDay;
    UpdateIntervalControls();
}
protected internal virtual void UpdateIntervalControls() {
    UnsubscribeControlsEvents();
    try {
        UpdateIntervalControlsCore();
    }
    finally {
        SubscribeControlsEvents();
    }
}
protected virtual void UpdateIntervalControlsCore() {
    edtEndDate.EditValue = controller.DisplayEnd.Date;
    edtEndTime.EditValue = new DateTime(controller.DisplayEnd.TimeOfDay.Ticks);
    //...
    bool enableTime = !controller.AllDay;
    edtEndTime.Visible = enableTime;
    edtEndTime.Enabled = enableTime;
}

В вебе валидация значений может осуществляется не в форме, а в callback-комманде, которая получает доступ к значениям редакторов. Произведя анализ введенных значений, может быть принято решение закрывать форму или нет и выдать информационное сообщение.

В результате наш контроллер формы инкапсулирует в себе весь необходимый функционал и будучи простым классом может быть повторно использован, будь то формы WinForms, веб или WPF/SL.

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

Использование контроллера форм


Проиллюстрируем фрагментами кода примеры использования AppointmentFormControllerBase на различных платформах.

1. Windows Forms

Экземпляр контроллера создан в конструкторе формы, редакторы проиницилизированы свойствами контроллера, при изменениях значений редакторов модифицируются свойства контроллера, присутствует код на применение изменений.
public partial class AppointmentForm : DevExpress.XtraEditors.XtraForm {
readonly AppointmentFormController controller;
 
public AppointmentForm(SchedulerControl control, Appointment apt, bool openRecurrenceForm) {
    // …
    this.controller = CreateController(control, apt);
    UpdateForm();
}
protected internal AppointmentFormController Controller { get { return controller; } }
protected virtual AppointmentFormController CreateController(SchedulerControl control, Appointment apt) {
    return new AppointmentFormController(control, apt);
}
protected virtual void UpdateForm () {
    tbSubject.Text = controller.Subject;
    edtShowTimeAs.Status = controller.GetStatus();
 
    bool resourceSharing = controller.ResourceSharing;
    edtResource.Visible = !resourceSharing;
    bool canEditResource = controller.CanEditResource;
    edtResource.Enabled = canEditResource;
    //…
}
protected internal virtual void tbSubject_EditValueChanged(object sender, EventArgs e) {
    controller.Subject = tbSubject.Text;
}
protected internal virtual void OnOkButton() {
    if (controller.IsConflictResolved()) {
  	controller.ApplyChanges();
    }
    //…
 }

2. ASP.NET

В вебе форма ввода является UserControl-ом и загружается как шаблон. Для «привязки» значений редакторов к свойствам контроллера используются Data Binding Expressions через синтаксис вида "<%# %>". Экземпляр контроллера создаётся в классе контейнера шаблона и доступен из формы через свойство Container.

ascx-файл формы выглядит следующим образом

<%@ Control Language="C#" AutoEventWireup="true" Inherits="AppointmentForm" CodeFile="AppointmentForm.ascx.cs" %>
<%@ Register Assembly="DevExpress.Web.ASPxEditors.v10.2, … Namespace="DevExpress.Web.ASPxEditors" TagPrefix="dxe" %>
 
<table class="dxscAppointmentForm">
</table>
  <tr>
  //…
    <td>
      <dxe:ASPxDateEdit ID="edtStartDate" runat="server" 
          Date='<%# ((AppointmentFormTemplateContainer)Container).Start %>' EditFormat="DateTime" />
    </td>
    <td>
      <dxe:ASPxComboBox ID="edtResource" runat="server" DataSource='<%# ResourceDataSource %>' 
          Enabled='<%# ((AppointmentFormTemplateContainer)Container).CanEditResource %>' />
     </td> 
      //…
</tr>
</table>
<dxsc:AppointmentRecurrenceForm ID="AppointmentRecurrenceForm1" runat="server" 
    Start='<%# ((AppointmentFormTemplateContainer)Container).RecurrenceStart %>' > 
</dxsc:AppointmentRecurrenceForm>
 //…
<dxe:ASPxButton runat="server" ID="btnOk" Text="OK" UseSubmitBehavior="false" />

Ниже представлен код шаблона формы и контейнера:
using System.Web.UI;

public partial class AppointmentForm : UserControl  {
public override void DataBind() {
    base.DataBind();
    AppointmentFormTemplateContainer container = (AppointmentFormTemplateContainer)Parent;
    AppointmentRecurrenceForm1.Visible = container.ShouldShowRecurrence;
    //…
    btnOk.ClientSideEvents.Click = container.SaveHandler;
}

public class AppointmentFormTemplateContainer : Control, IDataItemContainer, INamingContainer  {
AppointmentFormController controller;

public AppointmentFormTemplateContainer(ASPxScheduler control) {
    this.controller = CreateController(control, Appointment);
     // …
}
public DateTime Start { get { return TimeZoneHelper.ToClientTime(Controller.EditedAppointmentCopy.Start); } }
public bool CanEditResource { get { return Controller.CanEditResource; } }
public bool ShouldShowRecurrence { get { return Controller.SourceAppointment.IsOccurrence && Controller.ShouldShowRecurrenceButton; } }
public DateTime RecurrenceStart { 
    get { return TimeZoneHelper.ToClientTime(Controller.EditedPattern != null ? Controller.EditedPattern.RecurrenceInfo.Start : DateTime.Now); 
    } 
}
public string SaveHandler { get { return String.Format("function() {{ aspxAppointmentSave(\"{0}\"); }}", ControlClientId); } }
// …
}


3. WPF/SL

На данной платформе шаблон формы представлен System.Windows.Controls.UserControl-ом. Аналогично WinForms, экземпляр контроллера создаётcя в конструкторе формы. А вот «привязка» к значениям редакторов осуществляется в xaml через механизм двустороннего Binding-а.
<UserControl x:Class="DevExpress.Xpf.Scheduler.UI.AppointmentForm"
        	 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        	 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        	 xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"
        	 xmlns:dxsch="clr-namespace:DevExpress.Xpf.Scheduler"
        	 x:Name="AptForm" MinWidth="600" >
   // …
  <Grid>
        // …
        <dxe:TextEdit Text="{Binding Controller.Subject, UpdateSourceTrigger=PropertyChanged}" />
        <dxe:DateEdit Name="edtStartDate" EditValue ="{Binding Controller.DisplayStartDate}" />
       <!--Resources-->
        <TextBlock IsEnabled="{Binding Controller.CanEditResource}"/>
        <ContentControl Content="{Binding ElementName=AptForm, Path=Controller}" 
                            ContentTemplateSelector="{StaticResource ResTemplateSelector}"
                            IsEnabled="{Binding Controller.CanEditResource}">
       </ContentControl>

        <Button Click="OnDeleteButtonClick" 
                Visibility="{Binding Controller.CanDeleteAppointment, Converter={local:BoolToVisibilityConverter}}"  />
       // …
  </Grid>
</UserControl>


Итак, подведем итоги


Использование контроллера форм предоставляет следующие преимущества:
  • объединение всей бизнес-логики редактирования объекта в одном классе
  • возможность расширения редактируемых свойств, когда интерфейс объекта не соответствует интерфейсу формы ввода
  • возможность использования платформо-независимого объекта контроллера на различных платформах и уменьшение времени портирования кода с одной платформы на другую

Надеемся, что представленный в статье материал окажется для Вас полезным.
Tags:
Hubs:
+21
Comments 40
Comments Comments 40

Articles

Information

Website
www.developersoft.ru
Registered
Founded
1998
Employees
201–500 employees
Location
Россия