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 它是否已准备好关闭。