C# WPF MVVM开发框架Caliburn.Micro Screens, Conductors 和 Composition⑦
01
—
Screens, Conductors and Composition
Actions, Coroutines and Conventions往往最能吸引Caliburn.Micro的注意力,但如果你想讓你的UI設計得更好,那么了解屏幕和導體可能是最重要的。如果您想利用合成,這一點尤其重要。杰里米·米勒最近在為艾迪生·韋斯利撰寫《呈現模式》一書時,將屏幕、屏幕指揮和屏幕收藏這三個術語編成了法典。雖然這些模式主要通過從特定基類繼承ViewModels來在CM中使用,但將它們視為角色而不是視圖模型是很重要的。事實上,根據您的體系結構,屏幕可以是用戶控件、演示者或視圖模型。不過這有點超前了。首先,讓我們談談這些東西的一般含義。
Theory
Screen
這是最容易理解的結構。您可能認為它是應用程序表示層中存在的一個有狀態的工作單元。它獨立于應用程序外殼。外殼可能會顯示許多不同的屏幕,有些甚至同時顯示。shell可能也會顯示很多小部件,但它們不是任何屏幕的一部分。一些屏幕示例可能是應用程序設置的模式對話框、Visual Studio中的代碼編輯器窗口或瀏覽器中的頁面。你可能對此有很好的直覺。
通常情況下,屏幕具有與其相關聯的生命周期,允許屏幕執行自定義激活和停用邏輯。這就是杰里米所說的屏幕激活器。例如,以VisualStudio代碼編輯器窗口為例。如果在一個選項卡中編輯C#代碼文件,然后切換到包含XML文檔的選項卡,您會注意到工具欄圖標會發生變化。這些屏幕中的每一個都有自定義的激活/停用邏輯,使其能夠設置/拆除應用程序工具欄,以便它們根據活動屏幕提供適當的圖標。在簡單的場景中,ScreenActivator通常與Screen是同一個類。但是,您應該記住,這是兩個獨立的角色。如果特定屏幕具有復雜的激活邏輯,則可能需要將ScreenActivator考慮到其自己的類中,以降低屏幕的復雜性。如果您的應用程序具有許多不同的屏幕,但都具有相同的激活/停用邏輯,則這一點尤為重要。
Screen Conductor
一旦將屏幕激活生命周期的概念引入到應用程序中,就需要某種方法來實施它。這是屏幕指揮的角色。當您顯示屏幕時,導線會確保屏幕已正確激活。如果您正在從屏幕過渡,它會確保屏幕被停用。還有另一個場景也很重要。假設您有一個包含未保存數據的屏幕,并且有人試圖關閉該屏幕甚至應用程序。ScreenConductor已經在強制停用,它可以通過實現正常關機來提供幫助。與您的屏幕可能實現激活/停用界面的方式相同,它也可能實現一些界面,允許售票員詢問“您可以關閉嗎?”這引出了一個重要的問題:在某些情況下,停用屏幕與關閉屏幕相同,而在其他情況下,停用屏幕與關閉屏幕不同。例如,在VisualStudio中,當您從一個選項卡切換到另一個選項卡時,它不會關閉文檔。它只是激活/停用它們。必須顯式關閉選項卡。這就是觸發正常關機邏輯的原因。然而,在基于導航的應用程序中,離開頁面導航肯定會導致停用,但也可能導致該頁面關閉。這完全取決于您的特定應用程序的體系結構,您應該仔細考慮這一點。
Screen Collection
在像VisualStudio這樣的應用程序中,您不僅有一個ScreenConductor來管理激活、停用等,而且還有一個ScreenCollection來維護當前打開的屏幕或文檔列表。通過添加這一難題,我們還可以解決停用與關閉的問題。屏幕集合中的任何內容都保持打開狀態,但一次只有其中一項處于活動狀態。在像VS這樣的MDI風格的應用程序中,導體將管理在ScreenCollection成員之間切換活動屏幕。打開一個新文檔會將其添加到屏幕集合并切換到活動屏幕。關閉文檔不僅會停用文檔,還會將其從屏幕集合中刪除。所有這一切都取決于它是否正面回答了“你能關門嗎?”。當然,文檔關閉后,指揮需要決定ScreenCollection中的哪些其他項目應該成為下一個活動文檔。
Implementations
有很多不同的方法來實現這些想法。您可以從TabControl繼承并實現IScreenConductor接口,并直接在控件中構建所有邏輯。把它添加到你的IoC容器中,你就可以開始跑步了。您可以在自定義UserControl上實現IScreen接口,也可以將其實現為POCO,用作監控控制器的基礎。ScreenCollection可以是一個自定義集合,具有維護活動屏幕的特殊邏輯,也可以只是一個簡單的IList。
Caliburn.Micro實現
這些概念通過各種接口和基類在CM中實現,這些接口和基類主要用于構建ViewModels。讓我們來看看它們:
Screens
在Caliburn.Micro中,我們將屏幕激活的概念分解為幾個界面:
IActivate–表示實現者需要激活。此接口提供激活方法、IsActive屬性和激活事件,激活時應引發這些事件。
IDeactivate–表示實現者需要停用。此接口有一個Deactivate方法,該方法采用bool屬性,指示除禁用屏幕外是否關閉屏幕。它還有兩個事件:AttemptingDeactivation(應在停用前引發)和Deactivate(應在停用后引發)。
IGuardClose–表示實現者可能需要取消關閉操作。它有一種方法:CanClose。該方法是使用異步模式設計的,允許在做出密切決策時發生復雜的邏輯,如異步用戶交互。調用方將向CanClose方法傳遞一個操作。實現者應該在保護邏輯完成時調用該操作。Pass true表示實現者可以關閉,否則為false。
除了這些核心生命周期接口之外,我們還有一些其他接口可以幫助創建表示層類之間的一致性:
IHaveDisplayName–有一個名為DisplayName的屬性
INotifyPropertyChangedEx–此接口繼承標準INotifyPropertyChanged,并使用其他行為對其進行擴展。它添加了一個IsNotifying屬性(可用于關閉/打開所有更改通知)、一個NotifyOfPropertyChange方法(可調用該方法引發屬性更改)和一個Refresh方法(可用于刷新對象上的所有綁定)。
IObservableCollection–由以下接口組成:IList、INotifyPropertyChangedEx和INotifyCollectionChanged
IChild–由作為層次結構一部分或需要引用所有者的元素實現。它有一個名為Parent的屬性。
IViewAware–由需要了解其綁定到的視圖的類實現。它有一個AttachView方法,框架在將視圖綁定到實例時調用該方法。它有一個GetView方法,框架在為實例創建視圖之前調用該方法。這允許緩存復雜視圖,甚至復雜視圖解析邏輯。最后,當視圖附加到名為ViewAttached的實例時,應該引發一個事件。
由于某些組合非常常見,我們有一些方便的接口和基類:
PropertyChangedBase–實現INotifyPropertyChangedEx(從而實現INotifyPropertyChanged)。除了標準字符串機制之外,它還提供了一個基于lambda的NotifyOfPropertyChange方法,支持強類型更改通知。此外,所有屬性更改事件都會自動封送到UI線程。
BindableCollection–通過繼承標準ObservableCollection并添加INotifyPropertyChangedEx指定的其他行為來實現IObservableCollection。此外,此類確保所有屬性更改和集合更改事件都發生在UI線程上。
IScreen–此接口由其他幾個接口組成:IHaveDisplayName、IActivate、IDeactivate、IGuardClose和INotifyPropertyChangedEx。
Screen–繼承自PropertyChangedBase并實現IScreen接口。此外,還實現了IChild和iViewWare。
這意味著您可能會從PropertyChangedBase或Screen繼承大多數視圖模型。一般來說,如果您需要任何激活功能和PropertyChangedBase來完成其他一切,您將使用Screen。CM的默認屏幕實現還具有一些附加功能,可以輕松地連接到生命周期的適當部分:
OnInitialize–重寫此方法以添加僅在屏幕第一次激活時執行的邏輯。初始化完成后,IsInitialized將為true。
OnActivate–覆蓋此方法以添加每次激活屏幕時應執行的邏輯。激活完成后,IsActive將為true。
OnDeactivate–覆蓋此方法以添加自定義邏輯,該邏輯應在屏幕停用或關閉時執行。bool屬性將指示停用是否實際結束。停用完成后,IsActive將為false。
CanClose–默認實現始終允許關閉。重寫此方法以添加自定義保護邏輯。
OnViewLoaded–由于Screen實現了IViewAware,它借此機會讓您知道何時觸發視圖的Loaded事件。如果您遵循SupervisingController或被動查看樣式,并且需要使用視圖,請使用此選項。這也是放置視圖模型邏輯的地方,視圖模型邏輯可能依賴于視圖的存在,即使您可能沒有直接使用視圖。
TryClose–調用此方法關閉屏幕。如果屏幕由導體控制,它會要求導體啟動屏幕的關閉過程。如果屏幕不是由導體控制的,而是獨立存在的(可能是因為它是使用WindowManager顯示的),此方法將嘗試關閉視圖。在這兩種情況下,將調用CanClose邏輯,如果允許,將使用true值調用OnDeactivate。
所以,再重復一次:若你們需要一個生命周期,從屏幕繼承;否則從PropertyChangedBase繼承。
Conductors
正如我前面提到的,一旦引入生命周期,就需要一些東西來實施它。在Caliburn.Micro中,此角色由IConductor接口表示,該接口具有以下成員:
ActivateItem–調用此方法以激活特定項。如果導體使用“屏幕采集”,它也會將其添加到當前進行的項目中
DeactivateItem–調用此方法以停用特定項。第二個參數指示是否也應關閉該項。如果是這樣,如果導體使用“屏幕采集”,它也會將其從當前進行的項目中刪除
ActivationProcessed–在指揮處理項目激活時引發。它指示激活是否成功。
GetChildren–調用此方法返回導體正在跟蹤的所有項目的列表。如果導體使用“屏幕集合”,則返回所有“屏幕”,否則僅返回ActiveItem。(從iPart界面)
INotifyPropertyChangedEx–此接口由IConductor組成。
我們還有一個名為IConductActivieItem的接口,它由IConductor和IHaveActiveItem組成,用于添加以下成員:
ActiveItem–一個屬性,用于指示導體當前跟蹤的活動項目。
您可能已經注意到,CM的IConductor接口使用術語“項”而不是“屏幕”,我在引號中加了術語“屏幕集合”。原因是CM的導體實現不需要執行的項目來實現IScreen或任何特定接口。執行的項目可以是POCO。每個導體實現都是泛型的,對類型沒有約束,而不是強制使用IScreen。當要求導體激活/停用/關閉/等其正在執行的每個項目時,它會分別檢查它們是否存在以下細粒度接口:IActivate、IDeactivate、IGuardClose和IChild。實際上,我通常從Screen繼承已執行的項目,但這使您可以靈活地使用自己的基類,或者僅在每個類的基礎上實現所關心的生命周期事件的接口。您甚至可以讓一個導體跟蹤異構項,其中一些項繼承自屏幕,另一些項實現特定接口,或者根本沒有。
開箱即用的CM有三種IConductor實現,兩種與“屏幕集合”配合使用,另一種不配合使用。我們先來看看沒有收藏的售票員。
Conductor
這個簡單的導體通過顯式接口機制實現IConductor的大多數成員,并添加公開可用的相同方法的強類型版本。這允許通過接口以強類型方式(基于導體所執行的項目)處理導體。導體將停用和關閉視為同義詞。由于導線不保持“屏幕收集”,每個新項目的激活都會導致先前激活項目的停用和關閉。由于IGuardClose的異步性質以及傳導項可能實現或可能不實現此接口的事實,用于確定傳導項是否可以關閉的實際邏輯可能很復雜。因此,列車長將此委托給ICloseStrategy,ICloseStrategy負責處理此問題,并將查詢結果告知列車長。大多數情況下,您可以使用自動提供的DefaultCloseStrategy,但如果需要更改內容(可能IGuardClose不足以滿足您的需要),您可以將導體上的CloseStrategy屬性設置為您自己的自定義策略。
Conductor.Collection.OneActive
此實現具有導體的所有功能,但也添加了“屏幕集合”的概念。由于CM中的導體可以執行任何類型的類,因此此集合通過稱為Items而不是Screens的IObservableCollection公開。由于存在項目收集,已執行項目的停用和關閉不會被視為同義詞。激活新項目時,前一個激活項目僅被停用,并保留在“項目”集合中。要使用此導體關閉項,必須顯式調用其CloseItem方法。當項目關閉且該項目為激活項目時,指揮必須確定下一步應激活的項目。默認情況下,這是列表中上一個活動項之前的項。如果需要更改此行為,可以覆蓋DetermineExtItemToActivate。
Conductor.Collection.AllActive
類似地,此實現還具有Conductor的功能,并添加了“屏幕集合”的概念。主要區別在于,與單個項目同時處于活動狀態不同,許多項目可以處于活動狀態。關閉項目將停用該項目并將其從集合中移除。
關于CMs IConductor實現,我還沒有提到兩個非常重要的細節。首先,它們都繼承自屏幕。這是這些實現的一個關鍵特性,因為它在屏幕和導體之間創建了一個復合模式。假設您正在構建一個基本的導航樣式應用程序。您的shell將是導體的一個實例,因為它一次顯示一個屏幕,并且不維護集合。但是,假設其中一個屏幕非常復雜,需要一個多選項卡界面,每個選項卡都需要生命周期事件。嗯,這個特定的屏幕可能繼承自Conductor.Collection.OneActive。shell不需要考慮單個屏幕的復雜性。如果需要的話,其中一個屏幕甚至可以是實現IScreen而不是ViewModel的UserControl。第二個重要細節是第一個細節的結果。由于IConductor的所有OOTB實現都繼承自Screen,這意味著它們也有一個生命周期,生命周期級聯到它們正在執行的任何項目。因此,如果導體被停用,其活動項也將被停用。如果你試圖關閉一個導體,它將只能在它所執行的所有項目都可以關閉的情況下才能關閉。這是一個非常強大的功能。關于這一點,我注意到有一個方面經常絆倒開發人員**如果您在導體中激活了一個本身未激活的項目,則該項目在導體被激活之前不會被激活。**這一點在您思考時是有意義的,但偶爾會導致頭發拉扯。
Quasi-Conductors
在CM中,并不是所有可以成為屏幕的東西都植根于導體內部。例如,您的根視圖模型是什么?如果是指揮員,誰在激活它?這是引導程序執行的工作之一。引導程序本身不是引導者,但它理解上面討論的細粒度生命周期接口,并確保根視圖模型得到應有的尊重。WindowManager的工作方式與此類似,它的作用有點像一個指揮者,目的是強制執行模態(僅限非模態WPF)窗口的生命周期。所以,生命周期并不神奇。所有屏幕/導體必須植根于導體,或由引導程序或WindowManager管理,才能正常工作;否則,您將需要自己管理生命周期。
View-First
如果您正在使用WP7或Silverlight導航框架,您可能想知道是否/如何利用屏幕和導體。到目前為止,我一直在假設外殼工程主要采用ViewModel優先的方法。但是WP7平臺通過控制頁面導航來實施視圖優先的方法。SL Nav框架也是如此。在這些情況下,電話/導航框架就像一個導體。為了更好地使用ViewModels,WP7版本的CM有一個FrameAdapter,它與NavigationService掛鉤。這個適配器是由PhoneBootstrapper設置的,它理解導體所做的相同的細粒度生命周期接口,并確保在導航過程中在適當的時候在ViewModels上調用它們。您甚至可以通過在ViewModel上實現IGuardClose來取消手機的頁面導航。雖然FrameAdapter只是WP7版本的CM的一部分,但如果您希望將其與Silverlight導航框架結合使用,它應該可以方便地移植到Silverlight。
之前,我們在Caliburn.Micro中討論了屏幕和導體的理論和基本API。現在,我將介紹幾個示例中的第一個。此特定示例演示如何使用導體和兩個“頁面”視圖模型設置一個簡單的導航樣式shell。正如您從項目結構中看到的,我們有典型的Bootstrapper和ShellViewModel模式。為了使這個示例盡可能簡單,我甚至沒有使用帶引導程序的IoC容器。讓我們先看看ShellViewModel。它繼承自導體,實現如下:
以下是相應的ShellView:
請注意,ShellViewModel有兩個方法,每個方法都將視圖模型實例傳遞給ActivateItem方法。回想一下我們之前的討論,ActivateItem是導體上的一種方法,它將導體的ActiveItem屬性切換到此實例,并將實例推過屏幕生命周期的激活階段(如果它通過實現IActivate支持它)。還記得,如果ActiveItem已設置為實例,則在設置新實例之前,將檢查前一個實例是否實現了IGuardClose,這可能會取消ActiveItem的切換,也可能不會取消。假設當前ActiveItem可以關閉,那么導體將推動它通過生命周期的停用階段,將true傳遞給Deactivate方法以指示視圖模型也應該關閉。這就是在Caliburn.Micro中創建導航應用程序所需的全部內容。導體的ActiveItem表示“當前頁面”,導體管理從一個頁面到另一個頁面的轉換。這一切都是以ViewModel優先的方式完成的,因為驅動導航而不是“視圖”的是指揮家和子視圖模型
一旦基本導體結構就位,就很容易獲得它。ShellView演示了這一點。我們所要做的就是在視圖中放置ContentControl。通過將其命名為“ActiveItem”,我們的數據綁定約定開始生效。ContentControl的約定有點有趣。如果綁定到的項不是值類型,也不是字符串,那么我們假設內容是ViewModel。因此,我們沒有像在其他情況下那樣綁定到Content屬性,而是使用CM的自定義附加屬性:View.Model設置綁定。此屬性使CM的ViewLocator為視圖模型查找適當的視圖,并使CM的ViewModelBinder將兩者綁定在一起。完成后,我們將視圖彈出到ContentControl的Content屬性中。這個單一的約定使得框架中功能強大但簡單的ViewModel優先組合成為可能。
為了完整起見,讓我們看看PageOneViewModel和PageTwoViewModel:
Along with their views:
<UserControl x:Class="Caliburn.Micro.SimpleNavigation.PageOneView"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><TextBlock FontSize="32">Page One</TextBlock> </UserControl><UserControl x:Class="Caliburn.Micro.SimpleNavigation.PageTwoView"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><TextBlock FontSize="32">Page Two</TextBlock> </UserControl>我想指出最后幾點。請注意,PageOneViewModel只是一個POCO,但PageTwoViewModel繼承自Screen。請記住,CM中的導線不會對可以進行的操作施加任何限制。相反,他們會在必要的時候檢查每個實例是否支持各種細粒度生命周期實例。因此,當為PageTwoViewModel調用ActivateItem時,它將首先檢查PageOneViewModel以查看是否實現了IGuardClose。由于它沒有,它將嘗試關閉它。然后,它將檢查是否實現了IDeactivate。由于沒有,它將繼續激活新項目。首先,它檢查新項是否實現了IChild。因為Screen是這樣做的,所以它連接了層次關系。接下來,它將檢查PageTwoViewModel以查看是否實現了IActivate。因為Screen會這樣做,所以OnActivate方法中的代碼將運行。最后,它將在導體上設置ActiveItem屬性并引發適當的事件。這里有一個重要的結果應該記住:激活是一個特定于ViewModel的生命周期過程,不能保證任何有關視圖狀態的信息。很多時候,即使您的ViewModel已激活,其視圖也可能不可見。運行示例時,您將看到這一點。消息框將在激活發生時顯示,但第二頁的視圖仍不可見。請記住,如果您有任何依賴于已加載視圖的激活邏輯,則應覆蓋Screen.OnViewLoaded,而不是與OnActivate結合使用。
Simple MDI
讓我們看另一個例子:這一次是一個使用“屏幕集合”的簡單MDI shell。正如您再次看到的,我讓事情變得非常小和簡單:
下面是應用程序運行時的屏幕截圖:
這里我們有一個簡單的WPF應用程序,其中包含一系列選項卡。單擊“打開選項卡”按鈕會產生明顯的效果。單擊選項卡內的“X”將關閉該特定選項卡(也可能是顯而易見的)。讓我們通過查看ShellViewModel深入了解代碼:
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive {int count = 1;public void OpenTab() {ActivateItem(new TabViewModel {DisplayName = "Tab " + count++});} }由于我們希望維護一個打開項目的列表,但一次只保持一個項目處于活動狀態,因此我們使用Conductor.Collection.OneActive作為基類。注意,與前面的示例不同,我實際上是將已執行項的類型限制為IScreen。在這個示例中并沒有真正的技術原因,但這更接近于我在實際應用程序中的實際操作。OpenTab方法只需創建TabViewModel的一個實例,并設置其DisplayName屬性(來自IScreen),使其具有人類可讀的唯一名稱。讓我們思考幾個關鍵場景中導體與其屏幕之間的交互邏輯:
打開第一項
將項目添加到“項目”集合。
檢查項目是否存在IActivate,如果存在則調用它。
將項目設置為ActiveItem。
關閉現有項目
將該項傳遞給CloseStrategy,以確定是否可以關閉該項(默認情況下,它查找IGuardClose)。否則,操作將被取消。
檢查結束項是否為當前活動項。如果是,請確定下一步要激活的項目,并按照“打開其他項目”中的步驟進行操作
檢查結賬項目是否已激活。如果是這樣,則使用true調用以指示應該停用和關閉它。
從Items集合中刪除該項。
這些是主要的情況。希望你能看到一些不同的指揮家沒有收集,并理解為什么這些差異存在。讓我們看看ShellView如何渲染:
<Window x:Class="Caliburn.Micro.SimpleMDI.ShellView"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:cal="http://www.caliburnproject.org"Width="640"Height="480"><DockPanel><Button x:Name="OpenTab"Content="Open Tab" DockPanel.Dock="Top" /><TabControl x:Name="Items"><TabControl.ItemTemplate><DataTemplate><StackPanel Orientation="Horizontal"><TextBlock Text="{Binding DisplayName}" /><Button Content="X"cal:Message.Attach="DeactivateItem($dataContext, 'true')" /></StackPanel></DataTemplate></TabControl.ItemTemplate></TabControl></DockPanel> </Window>如您所見,我們使用的是WPF選項卡控件。CM的約定將其ItemsSource綁定到Items集合,將其SelectedItem綁定到ActiveItem。它還將添加一個默認ContentTemplate,用于在ActiveItem的ViewModel/View對中進行組合。約定還可以提供ItemTemplate,因為我們的選項卡都實現IHaveDisplayName(通過屏幕),但我選擇通過提供我自己的來啟用關閉選項卡來覆蓋它。我們將在后面的文章中更深入地討論約定。為完整起見,以下是TabViewModel及其視圖的簡單實現:
namespace Caliburn.Micro.SimpleMDI {public class TabViewModel : Screen {} }<UserControl x:Class="Caliburn.Micro.SimpleMDI.TabView"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><StackPanel Orientation="Horizontal"><TextBlock Text="This is the view for "/><TextBlock x:Name="DisplayName" /><TextBlock Text="." /></StackPanel> </UserControl>到目前為止,我一直試圖保持簡單,但我們的下一個樣本并非如此。在準備過程中,您可能希望至少仔細考慮或嘗試做以下事情:
擺脫常規的TabViewModel。在真正的應用程序中,您不會真的做這樣的事情。創建兩個自定義視圖模型和視圖。將對象連接起來,以便可以在導體中打開不同的視圖模型。當激活每個視圖模型時,確認在選項卡控件中看到正確的視圖。
在Silverlight中重建此示例。不幸的是,Silverlight的TabControl完全崩潰,無法充分利用數據綁定。相反,嘗試使用水平列表框作為選項卡,使用ContentControl作為選項卡內容。將它們放在DockPanel中,并使用一些命名約定,您將獲得與TabControl相同的效果。
創建工具欄視圖模型。添加IoC容器并將ToolBarViewModel注冊為singleton。將其添加到ShellViewModel,并確保在ShellView中呈現(請記住,您可以為此使用命名ContentControl)。接下來,將工具欄ViewModel插入到每個選項卡ViewModels中。在選項卡ViewModel OnActivate和OnActivate中編寫代碼,以便在激活特定選項卡ViewModel時從工具欄中添加/刪除上下文項。額外好處:創建一個DSL來完成這項工作,它不需要在激活覆蓋中使用顯式代碼。提示:使用事件。
取SimpleMDI樣本和SimpleNavigation樣本,并將它們組合在一起。在導航示例中將MDI外殼添加為PageViewModel,或在MDI示例中將導航外殼添加為選項卡。
Hybrid
此示例大致基于Billy Hollis在這部著名的DNR電視劇中展示的想法。與其花時間解釋UI的功能,不如看一下這個簡短的視頻,以獲得一個簡短的視覺解釋。
好的,現在您已經看到了它的功能,讓我們看看它是如何組合在一起的。正如您從屏幕截圖中看到的,我選擇按功能組織項目:客戶、訂單、設置等。在大多數項目中,我更喜歡這樣做,而不是按“技術”分組組織,如視圖和視圖模型。如果我有一個復雜的特性,那么我可能會將其分解為這些區域。
我不打算逐行檢查這個樣本。如果你花點時間仔細看看,自己弄清楚事情是如何運作的,那就更好了。但是,我想指出一些有趣的實現細節。
ViewModel Composition
Caliburn.Micro的屏幕和導體最重要的特征之一是,它們是復合模式的實現,使它們易于以不同的配置組合在一起。一般來說,組合是面向對象編程最重要的方面之一,學習如何在表示層中使用它可以帶來很大的好處。為了了解構圖在這個特定示例中的作用,讓我們看兩個屏幕截圖。第一個顯示視圖中包含CustomerWorkspace的應用程序,編輯特定客戶的地址。第二個屏幕是相同的,但其視圖/視圖模型對是三維旋轉的,因此您可以看到UI是如何組成的。
編輯客戶地址
編輯客戶地址(3D分支)
在此應用程序中,ShellViewModel是一個Conductor.Collection.OneActive。它在視覺上由窗口鍍鉻、標題和底部底座表示。碼頭有按鈕,每個正在進行的IWorkspace都有一個按鈕。單擊特定按鈕可使Shell激活該特定工作區。由于ShellView有一個綁定到ActiveItem的TransitionContentControl,激活的工作區被注入,其視圖顯示在該位置。在本例中,激活的是CustomerWorkspace視圖模型。CustomerWorkspaceViewModel恰好繼承了Conductor.Collection.OneActive。此ViewModel有兩個上下文視圖(請參見下文)。在上面的屏幕截圖中,我們顯示了詳細信息視圖。details視圖還有一個TransitionContentControl綁定到CustomerWorkspaceViewModel的ActiveItem,從而導致當前CustomerServiceWModel與其視圖一起組成。CustomerViewModel能夠顯示本地模式對話框(它們只是特定自定義記錄的模式對話框,而不是其他任何對話框)。這是由DialogConductor的實例管理的,DialogConductor是CustomServiceWModel上的一個屬性。DialogConductor的視圖覆蓋CustomerView,但僅當DialogConductor的ActiveItem不為null時才可見(通過值轉換器)。在上面描述的狀態中,DialogConductor的ActiveItem被設置為AddressViewModel的實例,因此模態對話框與AddressView一起顯示,并且基礎CustomerView被禁用。本示例中使用的整個shell框架就是以這種方式工作的,只需實現IWorkspace即可完全擴展。CustomerViewModel和SettingsViewModel是此接口的兩種不同實現,您可以深入研究。
同一ViewModel上的多個視圖
您可能不知道這一點,但是Caliburn.Micro可以在同一個ViewModel上顯示多個視圖。在View/ViewModel的注入站點上設置View.Context attached屬性可以支持這一點。以下是默認CustomerWorkspace視圖中的一個示例:
<clt:TransitioningContentControl cal:View.Context="{Binding State, Mode=TwoWay}"cal:View.Model="{Binding}" Style="{StaticResource specialTransition}"/>圍繞它還有許多其他Xaml,以形成CustomerWorkSpace視圖的chrome,但內容區域是視圖中最值得注意的部分。請注意,我們正在將View.Context附加屬性綁定到CustomerWorkspaceViewModel的State屬性。這允許我們根據該屬性的值動態更改視圖。因為這些都托管在TransitioningContentControl中,所以每當視圖發生更改時,我們都會得到一個很好的轉換。此技術用于將CustomerWorkSpace視圖模型從“主”視圖(其中顯示所有打開的CustomerViewModel)、搜索UI和新按鈕切換到“詳細”視圖,其中顯示當前激活的CustomerViewModel及其特定視圖(由中組成)。為了讓CM找到這些上下文視圖,您需要一個基于ViewModel名稱的名稱空間,減去單詞“View”和“Model”,其中一些視圖的名稱與上下文對應。例如,當框架查找Caliburn.Micro.HelloScreens.Customers.CustomersWorkspaceViewModel的詳細視圖時,它將查找Caliburn.Micro.HelloScreens.Customers.CustomersWorkspace.Detail,這是現成的命名約定。如果這不適用于您,只需自定義ViewLocator.LocateForModelType函數。
自定義IConductor實現
盡管Caliburn.Micro為開發人員提供了IScreen和IConductor的默認實現。很容易實現您自己的。在這個示例中,我需要一個對話框管理器,它可以是應用程序特定部分的模態,而不會影響其他部分。正常情況下,默認導體可以工作,但我發現我需要微調關機順序,所以我實現了自己的。讓我們看一看:
[Export(typeof(IDialogManager)), PartCreationPolicy(CreationPolicy.NonShared)] public class DialogConductorViewModel : PropertyChangedBase, IDialogManager, IConductActiveItem {readonly Func<IMessageBox> createMessageBox;[ImportingConstructor]public DialogConductorViewModel(Func<IMessageBox> messageBoxFactory) {createMessageBox = messageBoxFactory;}public IScreen ActiveItem { get; private set; }public IEnumerable GetChildren() {return ActiveItem != null ? new[] { ActiveItem } : new object[0];}public void ActivateItem(object item) {ActiveItem = item as IScreen;var child = ActiveItem as IChild;if(child != null)child.Parent = this;if(ActiveItem != null)ActiveItem.Activate();NotifyOfPropertyChange(() => ActiveItem);ActivationProcessed(this, new ActivationProcessedEventArgs { Item = ActiveItem, Success = true });}public void DeactivateItem(object item, bool close) {var guard = item as IGuardClose;if(guard != null) {guard.CanClose(result => {if(result)CloseActiveItemCore();});}else CloseActiveItemCore();}object IHaveActiveItem.ActiveItem{get { return ActiveItem; }set { ActivateItem(value); }}public event EventHandler<ActivationProcessedEventArgs> ActivationProcessed = delegate { };public void ShowDialog(IScreen dialogModel) {ActivateItem(dialogModel);}public void ShowMessageBox(string message, string title = "Hello Screens", MessageBoxOptions options = MessageBoxOptions.Ok, Action<IMessageBox> callback = null) {var box = createMessageBox();box.DisplayName = title;box.Options = options;box.Message = message;if(callback != null)box.Deactivated += delegate { callback(box); };ActivateItem(box);}void CloseActiveItemCore() {var oldItem = ActiveItem;ActivateItem(null);oldItem.Deactivate(true);} }嚴格地說,我實際上不需要實現IConductor來完成這項工作(因為我沒有將它組合成任何東西)。但我選擇這樣做是為了表示這個類在系統中扮演的角色,并盡可能保持體系結構上的一致性。實現本身非常簡單。導體主要需要確保正確激活/停用其項目,并正確更新ActiveItem屬性。我還創建了兩個簡單的方法來顯示對話框和消息框,這些對話框和消息框通過IDialogManager界面公開。該類在MEF中注冊為非共享,以便希望顯示本地模態的應用程序的每個部分都將獲得自己的實例,并能夠維護自己的狀態,如上面討論的CustomServiceWModel所示。
自定義策略
本示例最酷的特性之一可能是如何控制應用程序關閉。由于IShell繼承了IGuardClose,因此在引導程序中,我們只需覆蓋啟動并連接Silverlight的主窗口。關閉事件以調用IShell.CanClose:
protected override void OnStartup(object sender, StartupEventArgs e) {base.OnStartup(sender, e);if(Application.IsRunningOutOfBrowser) {mainWindow = Application.MainWindow;mainWindow.Closing += MainWindowClosing;} }void MainWindowClosing(object sender, ClosingEventArgs e) {if (actuallyClosing)return;e.Cancel = true;Execute.OnUIThread(() => {var shell = IoC.Get<IShell>();shell.CanClose(result => {if(result) {actuallyClosing = true;mainWindow.Close();}});}); }ShellViewModel通過其基類Conductor.Collection.OneActive繼承此功能。由于所有內置導體都有閉合策略,因此我們可以創建特定于導體的關機機制,并輕松地將其插入。以下是我們如何插入自定義策略:
[Export(typeof(IShell))] public class ShellViewModel : Conductor<IWorkspace>.Collection.OneActive, IShell {readonly IDialogManager dialogs;[ImportingConstructor]public ShellViewModel(IDialogManager dialogs, [ImportMany]IEnumerable<IWorkspace> workspaces) {this.dialogs = dialogs;Items.AddRange(workspaces);CloseStrategy = new ApplicationCloseStrategy();}public IDialogManager Dialogs {get { return dialogs; }} }以下是該戰略的實施情況:
public class ApplicationCloseStrategy : ICloseStrategy<IWorkspace> {IEnumerator<IWorkspace> enumerator;bool finalResult;Action<bool, IEnumerable<IWorkspace>> callback;public void Execute(IEnumerable<IWorkspace> toClose, Action<bool, IEnumerable<IWorkspace>> callback) {enumerator = toClose.GetEnumerator();this.callback = callback;finalResult = true;Evaluate(finalResult);}void Evaluate(bool result){finalResult = finalResult && result;if (!enumerator.MoveNext() || !result)callback(finalResult, new List<IWorkspace>());else{var current = enumerator.Current;var conductor = current as IConductor;if (conductor != null){var tasks = conductor.GetChildren().OfType<IHaveShutdownTask>().Select(x => x.GetShutdownTask()).Where(x => x != null);var sequential = new SequentialResult(tasks.GetEnumerator());sequential.Completed += (s, e) => {if(!e.WasCancelled)Evaluate(!e.WasCancelled);};sequential.Execute(new ActionExecutionContext());}else Evaluate(true);}} }我在這里做的有趣的事情是重用IResult功能來異步關閉應用程序。以下是自定義策略如何使用它:
檢查每個IWorkspace以查看它是否是IConductor。
如果為true,則獲取實現應用程序特定接口IHaveShutdownTask的所有已執行項。
通過調用GetShutdownTask檢索關機任務。如果沒有任務,它將返回null,所以將其過濾掉。
由于關機任務是IResult,因此將所有這些傳遞給SequentialResult并開始枚舉。
IResult可以將ResultCompletionEventArgs.wasCancelled設置為true以取消應用程序關閉。
繼續執行所有工作區,直到完成或取消。
如果所有IResults成功完成,將允許關閉應用程序。
如果存在臟數據,CustomerViewModel和OrderViewModel將使用此機制顯示模式對話框。但是,您也可以將其用于任意數量的異步任務。例如,假設您有一個長時間運行的進程,希望防止應用程序關閉。這也會很好地解決這個問題。
02
—
最后
原文標題:Caliburn.Micro Xaml made easy
原文鏈接:https://caliburnmicro.com/documentation/coroutines
翻譯:dotnet編程大全
C#技術群?:?添加小編微信mm1552923,備注:進群!
總結
以上是生活随笔為你收集整理的C# WPF MVVM开发框架Caliburn.Micro Screens, Conductors 和 Composition⑦的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Win11用户增长迅速!你升了吗?
- 下一篇: 官宣!.NET官网发布中⽂版