前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C# WPF MVVM开发框架Caliburn.Micro Screens, Conductors 和 Composition⑦

C# WPF MVVM开发框架Caliburn.Micro Screens, Conductors 和 Composition⑦

作者头像
用户9127601
发布2022-01-13 08:41:04
2.5K0
发布2022-01-13 08:41:04
举报
文章被收录于专栏:dotNET编程大全dotNET编程大全

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:

代码语言:javascript
复制
<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深入了解代码:

代码语言:javascript
复制
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如何渲染:

代码语言:javascript
复制
<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及其视图的简单实现:

代码语言:javascript
复制
namespace Caliburn.Micro.SimpleMDI {
    public class TabViewModel : Screen {}
}
代码语言:javascript
复制
<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视图中的一个示例:

代码语言:javascript
复制
<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的默认实现。很容易实现您自己的。在这个示例中,我需要一个对话框管理器,它可以是应用程序特定部分的模态,而不会影响其他部分。正常情况下,默认导体可以工作,但我发现我需要微调关机顺序,所以我实现了自己的。让我们看一看:

代码语言:javascript
复制
[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:

代码语言:javascript
复制
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继承此功能。由于所有内置导体都有闭合策略,因此我们可以创建特定于导体的关机机制,并轻松地将其插入。以下是我们如何插入自定义策略:

代码语言:javascript
复制
[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; }
    }
}

以下是该战略的实施情况:

代码语言:javascript
复制
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编程大全

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 dotNET编程大全 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01
    • Theory
      • Implementations
        • Simple MDI
          • Hybrid
          • 02
            • 最后
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档