天天看點

Caliburn.Micro 項目文檔(翻譯):Screens, Conductors and Composition

原文位址(項目說明文檔):[Documentation  Screens, Conductors and Composition]

http://caliburnmicro.codeplex.com/wikipage?title=Screens%2c%20Conductors%20and%20Composition&referringTitle=Documentation

Actions, Coroutines 和 Conventions 是在 Caliburn.Micro 中很值得關注的, 但是  Screens 和 Conductors 更需要深入了解,如果你的 UI 需要進行良好的設計. 他是非常重要的一個利器。Screen, Screen Conductor 和 Screen Collection 這三個術語,Jeremy Miller 先生也多次在 "Presentation Patterns" for Addison Wesley 書籍中的到過。 而這些模式在CM中是通過從特定的基類繼承ViewModel實作的,最好了解它們為“角色(Roles)”,而不是普通的 View-Models。實際上根據你不同的架構,一個 Screen 可能是一個 UserControl, Presenter 或 ViewModel。有點兒跑題了, 我們先看一下它們是什麼吧。

一、概述:

1.Screen

最簡單易懂的說。你可能會認為它是一個在應用程式中有狀态的表示層單元。它獨立于應用程式shell。這種shell 可以顯示許多不同的screens,一些事件在某個時間可能會有不同的幾個screens,它可以顯示大量的小部件,但不是screens的某部分。一些 screen 執行個體還可能是用于程式設定的“模态對話框”。Visual Studio 中的 代碼編輯器視窗,或一個浏覽器頁。讀到這裡,你可能稍微有些了解。

通常一個Screen有一個與之關聯的生命周期,允許執行自定義的螢幕 激活/失效( activation/deactivation) 的邏輯。Jeremy 先生稱之為ScreenActivator。 以Visual Studio代碼編輯器視窗舉例。我在其中一個編輯器視窗寫代碼,之後我切換到了另一個頁籤編輯xml檔案,此時要注意工具欄圖示會發生改變。

每一個Screen都有自定義的 激活/失效 的邏輯,使它能夠 設定/解除安裝 應用程式工具欄等。這樣就能基于活動的screen為程式提供圖示。普通形況下,ScreenActivator 就是一個 Screen 類。 但是要記住它們是不同的角色。如果一個特定的 screen 有複雜的激活邏輯,為了減少複雜度,ScreenActivator 就是這個類中必要的因素了。如果你的程式有不同的Screen但有相同的 激活/失效 邏輯,這是很重要的一點。

2.Screen Conductor

隻要你的應用程式引入了 Screen 激活生命周期的概念。就需要執行某種方法,他就是ScreenConductor角色。 當顯示一個 screen時,conductor 會确認它的激活狀态。如果要離開一個 screen,它就可以實作停用功能。

同樣,Screen 的 激活/失效 可能要實作一個接口或多個接口,允許 Conductor 問它“你能關閉嗎?”。這就引出了一個重要的觀點:在某些情況下關閉 Screen 和别的Screen 可能是相同的,也可能是不同的,這裡就不一樣了。比如:在Visual Studio中,從一個頁籤切換到另一個頁籤中,隻是 激活/非激活 這兩個情況,還要顯示定義關閉頁籤邏輯,來完成觸發器邏輯。

然而,在基于導航(navigation)的應用程式中,跳轉導航肯定會非激活頁面,但也可能導緻頁面關閉。 這一切都取決于您的特定應用程式的體系結構,你應該仔細考慮這些情況。 

3.Screen Collection

 像Visual Studio這樣的應用程式,不僅會有一個ScreenConductor管理 激活/非激活 等等。也會有 ScreenCollection 維持目前打開的 Screens 或 Documents 的清單。 就像堆積木一樣,我們也可以解決 非激活/關閉 的問題。 所有ScreenCollection都是開放的,但隻有其中一個項目是活躍的。 MDI-style風格的應用程式(如VS,,Conductor 會管理 ScreenCollection 成員中的 Screen 的切換。 打開一個新 Document 會将它添加到 ScreenCollection 并切換到活躍的 Screen。 關閉文檔不僅會關閉它,還會從 ScreenCollection 中删除它。 這一切取決于是否去積極響應“要關閉它麼?”事件。 當然,檔案關閉後,Conductor 還需要決定ScreenCollection中的那些物件會成為下一個活動項。

二、實作:

有很多不同的實作方式。 你可以繼承一個TabControl,實作一個IScreenConductor接口直接建構所有的邏輯的控件。 然後添加到您的IoC容器中,完成并運作。 也可以讓你的 UserControl(自定控件) 實作一個 IScreen 接口或者你可以實作它作為POCO讓 “監視控制器” 繼承。 

ScreenCollection可以是包含維護 Screen 活動狀态,一些特殊邏輯的自定義集合,或者它可能隻是一個簡單的 IList < IScreen >。

1.Caliburn.Micro 實作

通過CM中各種接口和基類實作,可以使用 mostly1to 建立 ViewModels。 讓我們看一看: 見注釋1

1.1 Screens

在Caliburn.Micro。 我們将 screen 激活的概念分解為幾個接口:  

  • IActivate – 表明實作者需要激活。 這個接口提供了一個 Activate 激活方法,一個 IsActive 屬性和一個 Activated 激活事件在發生激活時送出通知。
  • IDeactivate –表明實作者需要非激活。 這個接口有一個傳遞 bool 類型參數的 Deactivate 停用方法,可以定義是要關閉還是隻是停用它。它還有兩個事件:AttemptingDeactivation停用前通知事件。Deactivated停用後通知事件。
  • IGuardClose –表明實作者可能需要取消關閉操作。 它有一個 CanClose 方法。 該方法設計了一種異步模式,允許複雜的邏輯,如異步使用者互動時發生的關閉動作。 調用者将通過傳遞 Action<bool> 給CanClose方法。 實作者應該在監視到邏輯完成後,調用這個方法。 通過傳回的結果,來決定是否可以關閉。 

除了這些核心生命周期接口,我們有一些其他輔助建立類似的表示層的類: 

  • IHaveDisplayName – 有一個 DisplayName 屬性
  • INotifyPropertyChangedEx – 從标準 INotifyPropertyChanged 接口繼承而來,并增加額外的行為。 增加了一個 IsNotifying 屬性可以用來關閉/更改通知,增加 NotifyOfPropertyChange 方法用于手動調用所有屬性更新通知方法,用來重新整理所有綁定的對象。
  • IObservableCollection<T> – 由以下幾個接口組成: IList<T>, INotifyPropertyChangedEx, INotifyCollectionChanged
  • IChild<T> – 實作元素層次結構的一部分,或者需要一個引用一個所有者。 它有一個 Parent 屬性。
  • IViewAware – 這個類一定會注意的視圖的實作。 它有一個 AttachView 方法稱為架構綁定到視圖執行個體。 它有一個 GetView 方法在架構調用之前建立一個視圖執行個體。 它能緩存複雜的視圖,甚至複雜的視圖解析邏輯。 最後,它有一個 ViewAttached 事件,當視圖連接配接到執行個體時會自動通知。

因為某些組合非常普遍,我們提供了一些友善的接口和基類: 

  • PropertyChangedBase——實作INotifyPropertyChangedEx(也就是INotifyPropertyChanged)。 它提供了一個 lambda-based 的 NotifyOfPropertyChange方法,除了标準的字元串機制,還支援強類型的更改通知 。 同時,所有屬性改變事件自動整理到UI線程2。(見注釋2)
  • BindableCollection——實作 IObservableCollection < T > 的标準繼承 ObservableCollection < T > 還添加了 INotifyPropertyChangedEx 規定的行為。 同時此類還確定發生在UI線程集合所有屬性變化和更改事件。(見注釋2)
  • IScreen——這個接口由其他幾個接口組成:IHaveDisplayName, IActivate, IDeactivate,IGuardClose, INotifyPropertyChangedEx
  • Screen——從 PropertyChangedBase 繼承和實作 IScreen 接口。 此外還有 IChild 和 IViewAware 實作。

這意味着,你可能會從PropertyChangedBase或Screen繼承你自己的視圖模型。 一般來說,如果你需要任何激活特性和PropertyChangedBase的一切,您将使用Screen。CM預設的 Screen 實作一些附加功能,很容易hook到相應部分的生命周期: 

  • OnInitialize - 可重寫,僅在第一次激活 Screen 時執行的邏輯。 初始化完成後,IsInitialized 将為 true。
  • OnActivate—— 可重寫,每次 Screen 被激活後要執行的邏輯。 激活完成後, IsActive 将為 true。
  • OnDeactivate—— 可重寫,當螢幕以 “deactivated”或“closed”關閉時,應該執行的自定義邏輯。bool 類型的傳回值訓示該Screen如果還為alive狀态,則會傳回true
  • CanClose——可重寫,預設實作總是允許關閉。 重寫這個方法來添加自定義邏輯。
  • OnViewLoaded——因為 Screen 實作 IViewAware,他提供了一個機會讓你知道你的視圖的 Loaded 加載事件被觸發了。 如果你是 SupervisingController 或 PassiveView 風格,需要使用 View 視圖,就需要它。 這也是一個可能依賴于一個 view 的,放置 view model 邏輯的地方,即使你可能沒有直接使用view。
  • TryClose——調用這個方法關閉Screen。 如果Screen是由 Conductor 控制的,它要求Conductor 啟動Screen關閉過程。 如果Screen不是由Conductor 控制,但是獨立的(也許它通過 WindowManager 展示),該方法會試圖關閉視圖。 在這兩種場景 CanClose 邏輯将被調用,如果通過,OnDeactivate 将被設為true。 

要重申:如果你需要一個生命周期,就繼承 Screen,不然就繼承 PropertyChangedBase。

1.2 Conductors

如上所述,隻要引用生命周期,就一定會實施一些東西。在Caliburn.Micro中,這一角色由 IConductor 接口表示,具有以下成員: 

    • ActivateItem – 激活一個特定的項目。如果 conductor 使用了 “screen collection”,他也會被加載到目前控制集合中。
    • DeactivateItem – 禁用特定項目。第二個參數表明項目是否會被關閉。定義為關閉時,如果 conductor 使用了 “screen collection”,會把它從目前控制集合中删除。
    • ActivationProcessed – 通知 conductor已經完成激活一個項目,不論是否激活成功3 。 
    • GetChildren– 傳回 conductor 跟蹤的所有清單項。如果 conductor 使用了 “screen collection”, 會傳回所有的 “screens”, 其它方式隻會傳回 ActiveItem. (來自 IParent 接口)
    • INotifyPropertyChangedEx – 此接口組成 IConductor.

我們也有一個接口稱為 IConductActiveItem 構成 IConductor 和 IHaveActiveItem ,有以下成員: 

    • ActiveItem – 該屬性表明 conductor 正在跟蹤的目前活動項目。

 it checks them individually for the following fine-grained interfaces: IActivate, IDeactive, IGuardClose and IChild. In practice, I usually inherit conducted items from Screen, but this gives you the flexibility to use your own base class, or to only implement the interfaces for the lifecycle events you care about on a per-class basis. You can even have a conductor tracking heterogeneous items, some of which inherit from Screen and others that implement specific interfaces or none at all.

您可能已經注意到,CM的 IConductor 接口使用術語 “item” ,而不是 “screen” ,我給 “screen collection” 這個詞加上了引号。 原因是 CM 的 conductor  的實作不需要欄目項(item)一定要實作 IScreen 或其它特别的的接口。 他的欄目項(item)可以是 POCOs(注:可能是 Plain Old CLR Object 的 縮寫,簡單的普通對象,),不強制使用 IScreen。每個 conductor  實作是通用的,沒有限制的類型。

當 conductor 要求為每一個每個欄目項(item)進行 激活/禁用/關閉/等等……,它會分别的單獨檢查以下小的接口:IActivate,IDeactive,IGuardClose,IChild。 在實踐中,我通常會繼承 Screen,但這樣您可以靈活地使用自己的基類,或隻實施你關心的基礎類的 生命周期 接口事件。 你甚至可以用 conductor 跟蹤混合 欄目項(item),它們可能繼承 Screen 和實作其它特定的接口或根本沒有。 

CM 有三個 IConductor 開袋即食的的實作,兩個使用“screen collection”,另一個沒有。 我們從沒使用的開始看一下: 

Conductor<T>

這個 conductor 通過顯式接口機制,簡單的實作了 IConductor 的大部分成員,增加了同樣的強類公開方法公開版本供使用。 它允許通過接口以強類型方式以及基于他們正在欄目項(item)來使用 conductors。 Conductor<T> 會将 “失效/關閉” 同等對待, 由于 Conductor<T> 不保持 “screen collection”,每個新欄目項(item)的激活,都會導緻之前激活的項目的失效和關閉。

由于 IGuardClose 的異步性,确定目前 item 是否可以關閉,可能實際的邏輯會很複雜,但實際上目前進行的 item 也可能不會實作這個接口。是以,conductor 會委托一個 ICloseStrategy<T> 來處理,再通知 conductor 處理的結果。 大部分的時候,你就用自動提供的 DefaultCloseStrategy<T> 就好了,但你需要改變一些東西(也許對你而言 IGuardClose 不夠用),你可以在 Conductor<T> 裡設定 CloseStrategy 屬性,來定義自己的政策。 

Conductor<T>.Collection.OneActive

When a new item is activated, the previous active item is deactivated only and it remains in the Items collection. To close an item with this conductor, you must explicitly call its CloseItem method.4

同理,這個也一樣實作了 Conductor<T> ,并添加一個概念"screen collection"。主要的差別在于,它不是隻激活一個item,而是同時可以激活多個item。關閉一個item會使之失效,并從集合中删除。

There’s one aspect about this that I’ve noticed frequently trips up developers. If you activate an item in a conductor that is itself not active, that item won’t actually be activated until the conductor gets activated. This makes sense when you think about it, but can occasionally cause hair pulling.

還有兩個我沒有提到的CM的 IConductor 的實作非常重要的細節。首先,他們都是從 Screen 繼承的,這是這些實作的一個關鍵特性。因為它建立了一個 screens 和conductors 之間的組合模式。是以,我們可以說你是在建構一個基本的導航風格的應用程式。你的 shell 可以是 Conductor<IScreen> 的一個執行個體,因為它隻顯示一個 Screen ,沒有要維護的清單。但是,這些 screens 的每一個都是非常複雜的,還需要有一個多标簽界面,每個标簽都需要有生命周期事件。

好吧,那就繼承 Conductor<T>.Collection.OneActive 吧。shell 不需要關心個别 screen 的複雜性。如果需要,某個 screen 可能是一個實作了 的 UserControl ,而不是一個 ViewModel 。

第二個重要的細節是第一條的推論。所有繼承 Screen  實作了 IConductor 接口的 OOTB(開袋即食)的對象,意味着他們也有一個生命周期,這個生命周期會級聯到所有他們正在管理的items項。是以,如果一個 conductor 被定義為失效(deactivated),他的 ActiveItem 也會失效停用。如果您嘗試關閉一個 conductor,也會關閉他管理的所有可以關閉的 items 。這是一個非常強大的功能。有關于這一點,我已經注意到這是經常絆倒開發者的一個方面。如果你在一個 conductor 中激活一個 item ,而 conductor 本身沒有被激活,該 item 實際上不會被激活,直到該 conductor 被激活。,但很可能會讓你想破頭的。

1.3Quasi-Conductors

在CM中,一個 Conductor 裡的頂層并非全都是都是一個 screen 。舉例來說,你的根 ViewModel 怎麼樣?如果它是一個 conductor,誰來激活它?嗯,這是 Bootstrapper 引導程式要幹的事兒。Bootstrapper 本身不是一個 conductor,但它可以了解為上面讨論的小生命周期接口,確定你的根 ViewModel 得到應有的支援。WindowManager 的方式5就有點兒像一個 conductor 維護你的模态視窗(或者僅适用于WPF的非模态視窗)的生命周期。是以,生命周期并不神奇。所有的 screens/conductors 必須要麼一個 conductor 的首節點,要麼是 Bootstrapper 或 WindowManager 的,才能正常工作。不然你還需要自己管理生命周期。

View-First

如果你正在使用WP7或使用Silverlight的導航架構,你可能想知道怎麼使用 screens 和 conductors。到目前為止,我一直假設 ViewModel-First 方式為主的 shell 工程。但是WP7平台是執行 View-First 的方法控制頁面導航。SL導航架構也是這樣。這種情況下,Phone/Nav(電話/導航)架構的行為像是一個 conductor。為了與ViewModels搭的好,CM的wp7版本有一個 它會hooks到 NavigationService。這個轉換器是由 PhoneBootstrapper 設定的,了解了和 conductors 相同的小生命周期接口,確定他們在适當的導航點,調取你的 ViewModels 。你甚至可以通過在 ViewModel 上實作 IGuardClose 取消手機的gage導航。而 FrameAdapter 隻是CM的wp7版本的一塊,如果你結合Silverlight導航架構使用它,移植也很容易。

Simple Navigation(最簡單的導航)

Caliburn.Micro 項目文檔(翻譯):Screens, Conductors and Composition

 Previously, we discussed the theory and basic APIs for Screens and Conductors in Caliburn.Micro. Now I would like to walk through the first of several samples. This particular sample demonstrates how to set up a simple navigation-style shell using Conductor<T> and two “Page” view models. As you can see from the project structure, we have the typical pattern of Bootstrapper and ShellViewModel. In order to keep this sample as simple as possible, I’m not even using an IoC container with the Bootstrapper. Let’s look at the ShellViewModel first. It inherits from Conductor<object> and is implemented as follows:

public class ShellViewModel : Conductor<object> {
    public ShellViewModel() {
        ShowPageOne();
    }

    public void ShowPageOne() {
        ActivateItem(new PageOneViewModel());
    }

    public void ShowPageTwo() {
        ActivateItem(new PageTwoViewModel());
    }
}      

Here is the corresponding ShellView:

<UserControl x:Class="Caliburn.Micro.SimpleNavigation.ShellView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:tc="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit">
    <tc:DockPanel>
        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    tc:DockPanel.Dock="Top">
            <Button x:Name="ShowPageOne"
                    Content="Show Page One" />
            <Button x:Name="ShowPageTwo"
                    Content="Show Page Two" />
        </StackPanel>

        <ContentControl x:Name="ActiveItem" />
    </tc:DockPanel>
</UserControl>      

未完待續……

已完成50%

繼續閱讀