ViewModel生命周期
一個好的起點是查看 ViewModel 生命周期。
想象一個選項卡式界面 - 類似於Visual Studio,它有一個shell(包含菜單,工具欄等)和一個包含編輯器選項卡的TabControl。在 Stylet 中,每個編輯器選項卡都將由其自己的 ViewModel 提供支持。
現在,其中一個 ViewModel 將通過實例化來開始其生命。接下來,將顯示它。之后,它可能會顯示或隱藏,具體取決於當前處於活動狀態的選項卡,然后最終關閉。在關閉之前,它有機會阻止關閉以提示您保存文件。
簡而言之,這是 ViewModel 的生命周期:它已創建,然后激活(顯示給用戶)。之后,它可以被停用(仍然活着但未顯示)並再次激活任意次數,然后最終被關閉(在被問及它是否准備好關閉之后)。
IDisposable
如果ViewModel實現了IDisposable,那么在其被父類關閉后將自動釋放(除非父類的DisposeChildren的屬性為false).
Conductors介紹
現在,ViewModel 不會神奇地知道它何時顯示、隱藏或關閉。必須告訴它。這是指揮的角色。
簡單地說,Conductors是一個視圖模型,它擁有另一個視圖模型,並且知道如何管理其生命周期。
在我們的Visual Studio示例中,Conductor將是ViewModel,它擁有TabControl,其中顯示了編輯器ViewModels,因此可能是Shell ViewModel。每當用戶選擇新的編輯器選項卡時,Conductor 都會停用舊選項卡,並激活新選項卡。當用戶關閉選項卡時,Conductor 將告訴該選項卡它已關閉,然后確定要顯示的下一個選項卡,並激活該選項卡。
ViewModels 有一個生命周期,它由擁有 ViewModel 的 Conductor 實現。
到目前為止,這已經非常抽象了 - 讓我們進入細節。
IScreen和Screen
正如我們在上面看到的,ViewModel 的生命周期由該 ViewModel 上的 Conductor 調用方法進行管理。這些方法在一組獨立的接口中定義 - 如果您實現了該接口,並且 ViewModel 的管理對象是 Conductor,則將調用該方法。如果需要,您可以選擇所需的接口。
有一個調用的總體接口IScreen來組成它們,還有一個名為Screen 的默認實現。這表現得非常好,你可能永遠不需要實現自己的。
- IScreenState:用於激活、停用和關閉 ViewModel。具有Activate 、Deactivate 和 Close方法,以及用於跟蹤屏幕狀態更改的事件和屬性。
- IGuardClose:用於詢問 ViewModel 是否可以關閉。有一個方法CanCloseAsync。
- IViewAware:有時 ViewModel 需要了解其視圖(何時附加、它是什么等)。此接口通過屬性View和方法AttachView允許這樣做。
- IHaveDisplayName:有一個DisplayName屬性。這個名稱被用作使用窗口管理器顯示的窗口和對話框的標題,對於像TabControls這樣的東西也很有用。
- IChild:對於 ViewModel 來說,知道 Conductor 在管理它的是什么(例如,請求關閉它)可能是有利的。如果 ViewModel 實現了IChild ,它將被告知這一點。
請注意,無法保證調用"激活"、"停用"和"關閉"的順序 - ViewModel 可以連續激活兩次,然后關閉而不停用。由 ViewModel 來注意這些事情,並做出相應的反應。Stylet's Screen就是這樣做的。
Screen有一些虛擬方法,如果你願意,我們鼓勵你覆蓋:
- OnInitialActivate:第一次激活屏幕時調用,並且永遠不會再調用。對於設置您不想在構造函數中設置的內容非常有用。
- OnActivate:在屏幕激活時調用。僅當屏幕尚未激活時才會被調用。
- OnDeactivate:在屏幕停用時調用。僅當屏幕尚未停用時才會被調用。
- OnClose:在屏幕關閉時調用。只會被調用一次。僅在屏幕停用時調用。
- OnViewLoaded:在觸發 View 的Loaded事件時調用。
- CanCloseAsync:當Conductor想知道Screen是否可以關閉時調用,默認情況下,返回Task.FromResult(this.CanClose).但您可以在此處添加自己的異步邏輯。
- CanClose:默認情況下調用CanCloseAsync。如果要決定是否可以同步關閉,請覆蓋CanClose 。如果要異步決定,請覆蓋 CanCloseAsync。
- RequestClose(bool? dialogResult = null):當您想向自己的Conductor請求關閉時,您可以調用此方法。如果需要在對話框中顯示,則使用 DialogResult 參數。
Screen 派生自PropertyChangedBase,因此很容易引發 PropertyChanged 通知。
您可能會發現所有 ViewModels 都是 Screen 的子類。這並不是說它們需要 - 您可以創建自己的實現,或者從上面選擇要實現的接口 IScreen- 但它既方便又強大。
有關Conductors的詳細描述
Conductors有各種風格,每種風格都有自己的用例。Conductors可以擁有單個 ViewModel(想想一次顯示一個頁面的導航),也可以擁有多個 ViewModel,但一次只能有一個活動模型(想想上面 Visual Studio 示例中的 TabControl),也可以是所有 ViewModels(想想具有大量獨立元素的網格)。Conductors還可以添加行為,例如保持一個顯示ViewModel的記錄(對於導航很有用)。
與Screen類一樣,Stylet 定義了許多Conductor感興趣的接口,以及許多實現(取決於所需的Conductor行為類型),盡管您當然可以實現自己的接口。
主接口是 IConductor
- ActivateItem(T item):獲取給定的項目,然后將其激活。是否停用了先前的項目由Conductor決定。
- DeactivateItem(T item):獲取給定的項目,然后將其停用。是否激活另一個項目由Conductor決定。
- CloseItem(T item):取出給定的項目,然后將其關閉。這是否會導致另一個項目被激活以取代其位置是特定於Conductor的。
具有單個活動項(無論它們可能有多少個非活動項)的Conductor也實現IHaveActiveItem
所有內置Conductors都將各已經實現IChild接口的item的Parent屬性設置為其身。所有內置Conductors額外實現了IChildDelegate,這允許子項請求關閉(調用CloseItem)。默認Screen實現中,調用Screen.RequestClose將會導致Screen在其父類上調用CloseItem(前提是其父類實現IChildDelegate),這反過來又會導致其父級(如果存在)關閉它。
內置Conductor
Stylet內置了一些Conductor,它們以多種直觀的方式執行。
所有這些Condcutor都派生自Screen ,允許Conductor輕松擁有其他conductor。這意味着您可以以任何您想要的方式組成conductors和screens。
Conductor
這個非常簡單的Conductor擁有一個 ViewModel(類型T),該模型公開為 ActiveItem. ActivateItem方法用於將當前實例替換為新的 ViewModel 實例,並將激活新項並關閉舊項。每當 Conductor
當被問及是否可以關閉它(當CanCloseAsync被調用時)時,它會返回ActiveItem返回的任何內容,如果沒有ActiveItem, 則返回 true。
也可以直接設置ActiveItem,這與調用ActivateItem具有相同的效果。
Conductor的 ViewModel 如下所示 - 綁定了到Conductor的 ActiveItem 屬性的ContentControl:
<Window x:Class="MyNamespace.ConductorViewModel"
xmlns:s="https://github.com/canton7/Stylet" ....>
<ContentControl s:View.Model="{Binding ActiveItem}"/>
</Window>
Conductor
.Collection.OneActive
該Conductor擁有許多items,但一次只能有一個item處於活動狀態。通過這種方式,它可以對 TabControl 的行為進行建模 - 許多選項卡可以同時存在,但一次只能顯示一個選項卡。
它擁有一個T類型的item的集合,其中一個被設置為ActiveItem。調用ActivateItem將添加的item傳遞到集合的Items,並且還會激活它並將其設置為ActiveItem ;如果之前設置了ActiveItem,則其舊值將被停用,並保留在集合中。
調用DeactivateItem或CloseItem某個項目將分別導致該項目被停用和關閉。由於它不再處於活動狀態,因此不能保持為ActiveItem - 而是選擇另一個項目作為 ActiveItem,並被激活並按此方式設置。默認情況下,新的ActiveItem位於集合Items中要停用/關閉的項前面。
如果需要,可以直接操作集合Items。也可以直接設置ActiveItem,這與調用ActivateItem具有相同的效果。
帶有使用此Conductor的 TabControl 的 ViewModel 可能如下所示(有關簡短版本,請參見下文):
<TabControl ItemsSource="{Binding Items}" SelectedItem="{Binding ActiveItem}" DisplayMemberPath="DisplayName">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
除了以上風格以外,Stylet還提供了一種可以做同樣的事情的風格。這意味着您可以改為執行以下操作:
<TabControl Style="{StaticResource StyletConductorTabControl}"/>
Conductor
.Collection.AllActive
這個Conductor與Conductor
調用DeactivateItem將就地停用該項目,而不會將其從集合Items中刪除。
也可以直接操作集合Items。添加的任何項目都將被激活,任何已刪除的項目都將被關閉。
典型的用例可能是使用 ItemsControl,其中所有項同時可見。以這種方式使用 ItemsControl 的 ViewModel 可能如下所示(同樣,有關簡短版本,請參閱下面的內容):
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
由於這非常冗長,Stylet 提供了一種樣式,用於設置以下屬性:
<ItemsControl Style="{StaticResource StyletConductorItemsControl}"/>
Conductor
.StackNavigation
這個Conductor是 Conductor
它有一個單一的ActiveItem,但也保留了過去活動的項目的(私有的)歷史記錄。激活新項目時,將停用前一個ActiveItem,並將其推送到歷史記錄堆棧。調用GoBack()將關閉當前ActiveItem ,並重新激活此歷史記錄堆棧中的頂部項目,並將其設置為新的 ActiveItem。
如果對當前ActiveItem調用CloseItem,則具有相同的效果。如果調用歷史記錄堆棧中存在的任何項目,則該項目將被關閉並從歷史記錄堆棧中刪除。調用Clear()將關閉並從歷史記錄堆棧中刪除所有項目。
WindowConductor
這個有點奇怪,因為它是內部的,你不需要直接與它互動,但為了引起興趣,我把它包括在這里。每當您使用WindowManager 顯示對話框或窗口時(這包括 Stylet 首次啟動應用程序時顯示的窗口)時,都會有一個新的WindowConductor來管理其生命周期。每當窗口或對話框最小化時,它都會被停用。每當它被最大化時,它就會被激活。如果您的 ViewModel 請求關閉它(見上文RequestClose),則WindowConductor會處理此問題。同樣,如果用戶自己關閉窗口,WindowConductor則會詢問您的 ViewModel 它是否已准備好關閉。