複雑なオブジェクトの編集フォームの後ろに隠されているものは何ですか?

この記事では、XtraSchedulerスケジューラに実装されているアプローチを引き続き紹介します。 前の記事で 、データシンクロナイザーについて説明しましたが、今回はフォームについて説明します。



アプリケーションでは、多くの場合、多数の依存プロパティを持つオブジェクトを入力または編集するように設計されたフォームを見つけることができます。 このような入力フォームの構築は、開発者にとって「頭痛」を引き起こします:エディターの投稿、初期化コードの作成、検証、イベントハンドラーの日常作業...

では、そのようなフォームを迅速かつ確実に作成する方法は?

編集可能なオブジェクト


編集されたオブジェクトの例として、スケジューラの特定のイベントを表すオブジェクトであるAppointmentを使用します。 このクラスの構造を次の図に示します。



選択したオブジェクトには、かなり大きなプロパティセットがあります。 同時に、すでに設定されている値に応じて、プロパティの特定の部分のみを編集可能にする必要があります。 さらに、オブジェクトは値を設定するためのビジネスロジックを実装します。1つのプロパティを変更すると、最初のプロパティに応じて、別のプロパティを設定/ゼロ化することができます。

したがって、フォームインターフェイスを開発するときは、これらすべての要因を考慮し、編集されたオブジェクトの状態の変化にインターフェイス要素を特定の方法で反応させる必要があります。

編集方法


このオブジェクトを編集するために、次のアプローチを使用しました。フォーム上のすべてのエディターは、オブジェクト自体のプロパティではなく、編集されたオブジェクトに関連付けられた特定のオブジェクトのプロパティを編集または表示します。

フォームインターフェイスはオブジェクト自体のインターフェイスとは異なる可能性があるため、コントローラーとアダプターパターンの使用について説明することをおそらく既に推測しているでしょう。

ソースの編集可能なオブジェクト、および編集に必要な他のオブジェクトのパラメーターとしてコンストラクターに渡すことにより、コントローラーを作成します。
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); } 

アクションの必要性を変更せずにフォームを閉じる場合は、消えます。 元のオブジェクトは変更されていません。

異なるアーキテクチャでは、変更を適用/ロールバックするためのスキームがここで説明したものと異なる場合があり、コピーの必要がない場合があることに注意してください。

コントローラーインターフェース


プロパティに移りましょう。 コントローラーでは、 Facadeパターンを実装し、必要なプロパティをコピーオブジェクトからコントローラープロパティに複製します。
 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); } } 

必要に応じて、たとえば、日付と時刻を個別に表示および編集するために、オブジェクトのプロパティを2つのコントローラープロパティに分割します。
 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; } 

Web上の値の検証は、フォームではなく、エディターの値にアクセスするコールバックコマンドで実行できます。 入力した値を分析した後、フォームを閉じるかどうかを決定し、情報メッセージを発行することが決定される場合があります。

その結果、フォームコントローラーは必要なすべての機能をカプセル化し、WinForms、Web、WPF / SLのいずれであっても、単純なクラスであるため再利用できます。

必要に応じて、プラットフォームごとに、プラットフォーム固有のアクションを実装するコントローラーの後継者が存在する場合があります。

フォームコントローラーを使用する


コードフラグメントを使用して、さまざまなプラットフォームでAppointmentFormControllerBaseを使用する例を示します。

1. Windowsフォーム

コントローラーインスタンスはフォームコンストラクターで作成され、エディターはコントローラープロパティで初期化され、エディター値が変更されるとコントローラープロパティが変更され、変更を適用するためのコードがあります。
 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

Webでは、入力フォームはUserControlであり、テンプレートとしてロードされます。 エディターの値をコントローラーのプロパティに「バインド」するには、「<%#%>」という形式の構文でデータバインディング式を使用します。 コントローラーインスタンスはテンプレートコンテナークラスで作成され、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と同様に、コントローラーインスタンスはフォームコンストラクターで作成されます。 ただし、エディターの値への「バインディング」は、双方向バインディングのメカニズムを通じてxamlで実行されます。
 <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> 

まとめると


フォームコントローラを使用すると、次の利点があります。

この記事に掲載されている資料が皆さんのお役に立てば幸いです。

Source: https://habr.com/ru/post/J107359/


All Articles