簡報選擇器

選擇適合特定領域物件的畫面

2004 年 8 月 31 日

這是 進一步的企業應用程式架構開發 的一部分,我在 2000 年代中期撰寫。很遺憾,自那時起,太多其他事情吸引了我的注意力,所以我沒有時間進一步處理它們,我也看不到未來會有太多時間。因此,這份文件仍處於草稿階段,直到我找到時間再次處理它之前,我不會做任何更正或更新。

當畫面之間的導覽由簡報引導時,簡報可以直接編碼要顯示的畫面順序。這樣一來,簡報就會知道每當發生合適的事件時,需要開啟哪個畫面。然而,有時應該顯示的畫面取決於領域中的資訊。簡報不知道要顯示哪個畫面,而是知道它應該在某個視窗中顯示領域物件。領域物件無法選擇畫面,因為那會違反 分隔簡報

簡報選擇器 解決了應該為特定領域物件使用哪個畫面。客戶端知道它應該開啟一個新畫面來顯示特定領域物件。客戶端詢問 簡報選擇器 應該為這個領域物件使用哪個畫面,然後開啟回傳的視窗。

運作方式

在最簡單的形式中,你可以將 簡報選擇器 視為一個字典查詢,它將領域物件類型與畫面類型建立索引。

Order => OrderWindow
Customer => CustomerWindow
PriorityCustomer => PriorityCustomerWindow
    

使用這個簡單模式,查詢適當的畫面類似於 aPresentationChooser[domainObject.type]

雖然這個簡單的概念是 簡報選擇器 的精髓,有些常見的區域會讓事情變得更複雜。其中大部分來自於雖然大多數的查詢完全基於網域物件的類型,但並非全部都是。因此,通常不值得將 簡報選擇器 的類型查詢本質公開給其用戶端,所以不要要求用戶端傳入網域物件的類型,而是傳入物件本身。這遵循一個通用原則,即物件絕不應要求用戶端執行它自己合理可執行的操作。它也允許 簡報選擇器 在需要時提供更精密的查詢規則。

簡報選擇器 通常是 服務。在初始化期間,一些組態模組會使用關於畫面和網域物件如何對應在一起的詳細資料初始化 簡報選擇器。然後,在正常執行期間,畫面會使用 簡報選擇器 來查詢畫面。

何時使用

當您必須使用不同的畫面類別(通常是視窗,但也可以是表單中的嵌入式面板)來回應相同的導覽流程時,您需要 簡報選擇器。一個簡單的範例,如下方我所使用的,是在單一清單中擁有不同類型的物件,而且您需要每個類型不同的畫面。

通常處理這種情況的最佳方式是使用相同類型的畫面,但在該畫面中使用隱藏和停用的控制項來封鎖對不適當資料的存取。如果類型之間的差異很小,而且您可以在沒有不協調的介面的情況下使用類似的畫面,那麼這會很有效。當畫面載入時,您會詢問網域物件來決定要啟用和顯示哪些控制項。

然而,使用相同的變數畫面會讓您在自訂顯示底層網域物件時有較少的選項。有時,相似性最終會讓使用者更困惑,而不是幫助他們。在這些情況下,最好使用完全不同的畫面,而 簡報選擇器 在此時就很有用,可以決定使用哪一個畫面。

雖然我這裡專注於根據網域資料變更畫面,但變更也可以包含整體自訂因素,例如使用者、使用者的關聯性、相同應用程式的多個提供者,或互動狀態的不同面向。所有這些自訂都可能導致動態使用不同的畫面類別,因此暗示在選擇要使用的實際畫面類別時需要間接處理。在這些情況下,簡報選擇器 是提供這種間接處理的良好方式。

「簡報選擇器」和 應用程式控制器 都將觸發導覽的畫面類別選項分開。「簡報選擇器」最適合用在要顯示的網域物件是主要變異時;「應用程式控制器」最適合用在應用程式的狀態是主要變異時。當兩者都變異時,結合這些模式是有意義的。

範例:簡單查詢 (C#)

「簡報選擇器」最簡單的範例是根據網域物件的類型,具備簡單查詢功能的範例。考慮一個顯示音樂錄音資訊的應用程式。錄音會顯示在清單中,任何錄音都可以編輯。然而,依據錄音是古典還是流行音樂,底層錄音需要以不同的方式編輯。

圖 1:挑選錄音的視窗。

為此,我有一個事件處理常式,用來處理編輯按鈕的按一下動作

private void btnEdit_Click(object sender, EventArgs e) {
  edit(Recordings[lstTitles.SelectedIndex]);
}
private void edit(IRecording recording) {
  Chooser.ShowDialog(recording);
}

這個模式的關鍵在於編輯方法,該方法會要求「簡報選擇器」顯示對話方塊,用來編輯選取的錄音,依據錄音是古典還是流行音樂而定。

在這個案例中,「簡報選擇器」其實只是一個字典查詢。在初始化期間,我們會載入「簡報選擇器」,其中包含網域類型和對應要使用的視窗的詳細資料。

class PresentationChooser...

  protected IDictionary presenters = new Hashtable();
  public virtual void RegisterPresenter(Type domainType, Type presentation) {
    presenters[domainType] = presentation;
  }

這個初始化只會將網域和簡報類型儲存在字典中。查詢程序稍微複雜一點。如果我稍後新增古典錄音的子類型,我希望它由古典錄音編輯,除非我已為該子類型註冊一個畫面。

class PresentationChooser...

  public RecordingForm ShowDialog (Object model) {
    Object[] args = {model};
    RecordingForm dialog = (RecordingForm) Activator.CreateInstance(this[model], args);
    dialog.ShowDialog();
    return dialog;
  }
  public virtual Type this [Object obj] {
    get {
      Type result = lookupPresenter(obj.GetType());
      if (result == null) 
        MessageBox.Show("Unable to show form", "Error", MessageBoxButtons.OK,  MessageBoxIcon.Error);
      return result;
    }
  }
  private Type lookupPresenter(Type arg) 
  {
    Type result = (Type)presenters[arg];
    return (null == result) ? lookupPresenter(arg.BaseType) : result;
  }

如果找不到已註冊的畫面,我已撰寫程式碼來顯示錯誤訊息對話方塊。當我註冊畫面時,我可以透過在階層頂端提供一個畫面來避免需要顯示這個錯誤訊息。

presentationChooser.RegisterPresenter(typeof(ClassicalRecording), typeof(FrmClassicalRecording));
presentationChooser.RegisterPresenter(typeof(PopularRecording), typeof (FrmPopularRecording));
presentationChooser.RegisterPresenter(typeof(Object), typeof (FrmNullPresentation));

範例:條件式 (C#)

在此,我採用上述的簡單範例,並允許更動態的選擇器行為,讓它變得更精緻一點。在此情境中,我希望大多數古典錄音都能以一般古典顯示形式顯示,但對於莫札特創作的任何作品,則使用特殊顯示形式。(我不確定為什麼我要這麼做,但我在星期二想不出令人信服的範例。)

大部分與 簡報選擇器 的介面都與較簡單的版本相同。顯示錄音對話框的程式碼仍然只是 Chooser.showDialog(aRecording)。不過,簡報選擇器 的實作會再複雜一點。

我仍然使用由類型索引的字典來儲存資訊。不過,這次我希望每個網域物件都有多個畫面。我可以使用類別擷取每個選項。

類別 DynamicPresentationChooser…

  Type _registeredType;
  Type _presentation;
  ConditionDelegate _condition;
  public delegate bool ConditionDelegate(Object recording); 
  public PresentationChoice(Type registeredType, Type presentation, ConditionDelegate condition)  {
    this._registeredType = registeredType;
    this._presentation = presentation;
    this._condition = condition;
  }

在此,我使用 C# 委派讓客戶端可以傳入布林函數,以針對網域物件評估條件。新的查詢功能會依序檢查清單中的所有選項,傳回第一個條件委派針對所提供網域物件評估為 true 的選項。

類別 PresentationChoice...

  public override Type this[Object obj] {
    get {
      IList list= presenterList(obj.GetType());
      return (null == list) ? typeof(FrmNullPresentation): chooseFromList(list, obj);
    }
  }
  private IList presenterList(Type type) {
    IList result = (IList) presenters[type];
    if (null != result)
      return result;
    else if (typeof (object) != type)
      return presenterList(type.BaseType);
    else
      return new ArrayList();
  }
  private static Type chooseFromList(IList list, object domainObject) {
    foreach (PresentationChoice choice in list) 
      if (choice.Matches(domainObject)) return choice.Presentation; 
    return typeof(FrmNullPresentation);
  }

如果沒有任何符合,我會傳回 空物件

為了註冊畫面,我決定要維護與上述簡單範例相容的介面。為此,我允許只使用網域和畫面類型進行註冊,就像以前一樣,透過幾個方便的方法來運作。

類別 DynamicPresentationChooser...

  public override void RegisterPresenter(Type domainType, Type presentation) {
    presenters[domainType] = new ArrayList();
    presenterList(domainType).Add (new PresentationChoice (domainType, presentation));
  }

類別 PresentationChoice...

  public PresentationChoice(Type domainType, Type presentation) : 
    this (domainType, presentation, null){
    _condition = new ConditionDelegate(TrueConditionDelegate);
  }
  public static bool TrueConditionDelegate(Object ignored) {return true;}

這樣一來,我可以在任何情況下使用動態選擇器,否則我會使用簡單選擇器。

然後,我使用其他註冊方法來輸入動態選項。我已設定這個方法,因此我只能在我已經有預設選項時執行此操作。這會讓組態介面比我想要的更難用,但這樣一來,任何問題都會顯示在組態中,因此結果會快速失敗。

類別 DynamicPresentationChooser...

  public void RegisterAdditionalPresenter(Type domainType, Type presentation, PresentationChoice.ConditionDelegate condition) {
    Debug.Assert(
      null != presenterList(domainType), 
      String.Format("Must register default choice for {0} first", domainType));
    presenterList(domainType).Insert(0, new PresentationChoice(domainType, presentation, condition));
  }

然後,我可以像這樣註冊類別。

presentationChooser.RegisterPresenter(typeof(ClassicalRecording), typeof(FrmClassicalRecording));
presentationChooser.RegisterPresenter(typeof(PopularRecording), typeof (FrmPopularRecording));
chooser.RegisterAdditionalPresenter(
  typeof(ClassicalRecording), 
  typeof(FrmMozartRecording), 
  new PresentationChoice.ConditionDelegate(MozartCondition));