C# WPF MVVM开发框架Caliburn.Micro IResult和协同程序⑥

2022-01-13 08:39:37 浏览数 (1)

引言部分,总领全篇文章的中心内容。

01

IResult and Coroutines

在前面,我提到了Actions概念的另一个引人注目的特性,称为协同程序。如果你以前没听说过这个词,下面是维基百科要说的:

在计算机科学中,协同程序是一种程序组件,它泛化子例程以允许多个入口点在某些位置暂停和恢复执行。协同程序非常适合实现更熟悉的程序组件,如协作任务、迭代器、无限列表和管道。

在计算机科学中,协同程序是一种程序组件,它泛化子例程以允许多个入口点在某些位置暂停和恢复执行。协同程序非常适合于实现更熟悉的程序组件,如协作任务、迭代器、无限列表和管道。这里有一种方法可以解决这个问题:想象能够执行一个方法,然后在某个语句上暂停它的执行,去做其他事情,然后返回并在您停止的地方继续执行。这种技术在基于任务的编程中非常强大,特别是当这些任务需要异步运行时。例如,假设我们有一个ViewModel,它需要异步调用一个web服务,然后它需要获取该结果,对其进行一些处理,并异步调用另一个web服务。最后,它必须在模式对话框中显示结果,并用另一个异步任务响应用户的对话框选择。使用标准的事件驱动异步模型实现这一点并不是一种愉快的体验。然而,这是一个使用协同程序来完成的简单任务。问题是……C#没有在本地实现协同路由。幸运的是,我们可以(某种程度上)在迭代器之上构建它们。

利用Caliburn.Micro中的这一特性需要两件事:首先,在某个类上实现IResult接口,表示您希望执行的任务;其次,从Action2生成IResult实例。让我们更具体一些。假设我们有一个Silverlight应用程序,我们希望动态下载并显示屏幕,而不是主包的一部分。首先,我们可能希望显示一个“加载”指示器,然后异步下载外部包,接下来隐藏“加载”指示器,最后导航到动态模块内的特定屏幕。如果您的第一个屏幕希望使用协同程序导航到动态加载的第二个屏幕,则代码如下所示:

代码语言:javascript复制
using System.Collections.Generic;
using System.ComponentModel.Composition;

[Export(typeof(ScreenOneViewModel))]
public class ScreenOneViewModel
{
    public IEnumerable<IResult> GoForward()
    {
        yield return Loader.Show("Downloading...");
        yield return new LoadCatalog("Caliburn.Micro.Coroutines.External.xap");
        yield return Loader.Hide();
        yield return new ShowScreen("ExternalScreen");
    }
}

首先,请注意动作“GoForward”的返回类型为IEnumerable。这对于使用协同程序是至关重要的。该方法的主体有四个收益率语句。每个收益都返回一个IResult实例。第一个是显示“下载”指示器的结果,第二个是异步下载xap,第三个是隐藏“下载”消息,第四个是显示下载的xap的新屏幕。在每个yield语句之后,编译器将“暂停”此方法的执行,直到特定任务完成。第一个、第三个和第四个任务是同步的,而第二个是异步的。但是yield语法允许您以顺序方式编写所有代码,将原始工作流保留为可读性和声明性更强的结构。要进一步了解其工作原理,请查看IResult接口:

代码语言:javascript复制
public interface IResult
{
    void Execute(CoroutineExecutionContext context);
    event EventHandler<ResultCompletionEventArgs> Completed;
}

这是一个实现起来相当简单的接口。只需在“Execute”方法中编写代码,并确保在完成时引发“Completed”事件,无论是同步任务还是异步任务。因为协同路由发生在动作内部,所以我们为您提供了一个ActionExecutionContext,它在构建与UI相关的IResult实现时非常有用。这允许ViewModel以声明的方式声明其控制视图的意图,而无需对视图进行任何引用,也无需进行基于交互的单元测试。以下是ActionExecutionContext的外观:

代码语言:javascript复制
public class ActionExecutionContext
{
    public ActionMessage Message;
    public FrameworkElement Source;
    public object EventArgs;
    public object Target;
    public DependencyObject View;
    public MethodInfo Method;
    public Func<bool> CanExecute;
    public object this[string key];
}

下面是对所有这些属性含义的解释:

Message

导致调用此IResult的原始ActionMessage。

Source

触发操作执行的框架元素。

EventArgs

与操作触发器关联的任何事件参数。

Target

存在实际操作方法的类实例。

View

与目标关联的视图。

Method

MethodInfo指定要在目标实例上调用的方法。

CanExecute

如果可以调用操作,则返回true,否则返回false的函数。

Key Index

存储/检索框架扩展可能使用的任何附加元数据的位置。

考虑到这一点,我编写了一个naive Loader IResult,它搜索VisualTree,查找用于显示加载消息的BusyIndicator的第一个实例。以下是实现:

代码语言:javascript复制
using System;
using System.Windows;
using System.Windows.Controls;

public class Loader : IResult
{
    readonly string message;
    readonly bool hide;

    public Loader(string message)
    {
        this.message = message;
    }

    public Loader(bool hide)
    {
        this.hide = hide;
    }

    public void Execute(CoroutineExecutionContext context)
    {
        var view = context.View as FrameworkElement;
        while(view != null)
        {
            var busyIndicator = view as BusyIndicator;
            if(busyIndicator != null)
            {
                if(!string.IsNullOrEmpty(message))
                    busyIndicator.BusyContent = message;
                busyIndicator.IsBusy = !hide;
                break;
            }

            view = view.Parent as FrameworkElement;
        }

        Completed(this, new ResultCompletionEventArgs());
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };

    public static IResult Show(string message = null)
    {
        return new Loader(message);
    }

    public static IResult Hide()
    {
        return new Loader(true);
    }
}

看看我是如何利用上下文的。查看?这在保持视图和视图模型之间的分离的同时打开了许多可能性。仅列出使用IResult实现可以做的一些有趣的事情:显示消息框、显示基于VM的模式对话框、在用户的鼠标位置显示基于VM的弹出窗口、播放动画、显示文件保存/加载对话框、基于VM属性而非控件将焦点放在特定的UI元素上等。当然,最大的机会之一是调用web服务。让我们看看您可能如何做到这一点,但通过使用稍微不同的场景,动态下载xap:

代码语言:javascript复制
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.ReflectionModel;
using System.Linq;

public class LoadCatalog : IResult
{
    static readonly Dictionary<string, DeploymentCatalog> Catalogs = new Dictionary<string, DeploymentCatalog>();
    readonly string uri;

    [Import]
    public AggregateCatalog Catalog { get; set; }

    public LoadCatalog(string relativeUri)
    {
        uri = relativeUri;
    }

    public void Execute(CoroutineExecutionContext context)
    {
        DeploymentCatalog catalog;

        if(Catalogs.TryGetValue(uri, out catalog))
            Completed(this, new ResultCompletionEventArgs());
        else
        {
            catalog = new DeploymentCatalog(uri);
            catalog.DownloadCompleted  = (s, e) =>{
                if(e.Error == null)
                {
                    Catalogs[uri] = catalog;
                    Catalog.Catalogs.Add(catalog);
                    catalog.Parts
                        .Select(part => ReflectionModelServices.GetPartType(part).Value.Assembly)
                        .Where(assembly => !AssemblySource.Instance.Contains(assembly))
                        .Apply(x => AssemblySource.Instance.Add(x));
                }
                else Loader.Hide().Execute(context);

                Completed(this, new ResultCompletionEventArgs {
                    Error = e.Error,
                    WasCancelled = false
                });
            };

            catalog.DownloadAsync();
        }
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
}

如果不清楚,此样本使用的是MEF。此外,我们正在利用为Silverlight 4创建的DeploymentCatalog。您不需要了解很多关于MEF或DeploymentCatalog的知识就可以获得外卖。请注意,我们连接DownloadCompleted事件,并确保在其处理程序中触发IResult.Completed事件。这就是使异步模式能够工作的原因。我们还确保检查错误并在ResultCompletionEventArgs中传递该错误。说到这里,这门课看起来是这样的:

代码语言:javascript复制
public class ResultCompletionEventArgs : EventArgs
{
    public Exception Error;
    public bool WasCancelled;
}

Caliburn.Micro的枚举器在从每个IResult回调后检查这些属性。如果出现错误或WASCELLENCEL设置为true,则停止执行。你可以利用这个优势。假设您为OpenFileDialog创建了一个IResult。您可以检查该对话框的结果,如果用户取消了该对话框,请在事件参数上设置wascelected。通过执行此操作,您可以编写一个动作,该动作假定如果执行Dialog.Show后面的代码,则用户必须选择了一个文件。这种技术可以简化这种情况下的逻辑。显然,如果需要,可以对SaveFileDialog或任何确认样式的消息框使用相同的技术。上面显示的LoadCatalog实现中我最喜欢的部分是,最初的实现是由CM用户编写的!感谢janoveh提交的这篇精彩文章!作为旁注,我们添加到CM项目站点的内容之一是“配方”部分。在未来几个月内,我们将在该领域添加更多类似的通用解决方案。因此,它将是一个检查酷插件和框架定制的好地方。

您可以做的另一件事是创建一系列围绕应用程序外壳构建的IResult实现。这就是上面使用的ShowScreen结果所做的。以下是它的实施:

代码语言:javascript复制
using System;
using System.ComponentModel.Composition;

public class ShowScreen : IResult
{
    readonly Type screenType;
    readonly string name;

    [Import]
    public IShell Shell { get; set; }

    public ShowScreen(string name)
    {
        this.name = name;
    }

    public ShowScreen(Type screenType)
    {
        this.screenType = screenType;
    }

    public void Execute(CoroutineExecutionContext context)
    {
        var screen = !string.IsNullOrEmpty(name)
            ? IoC.Get<object>(name)
            : IoC.GetInstance(screenType, null);

        Shell.ActivateItem(screen);
        Completed(this, new ResultCompletionEventArgs());
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };

    public static ShowScreen Of<T>()
    {
        return new ShowScreen(typeof(T));
    }
}

这带来了IResult的另一个重要特性。在CM执行结果之前,它会将结果传递给IoC.build方法,从而使容器有机会通过属性将依赖项推入。这允许您在视图模型中正常创建它们,同时仍然允许它们依赖于应用程序服务。在这种情况下,我们依赖于IShell。您还可以注入容器,但在本例中,我选择在内部使用IoC静态类。一般来说,你应该避免直接从容器中取出东西。但是,我认为在基础架构代码(如ShowScreen IResult)内部执行时,这是可以接受的。

其他用途

现成的Caliburn.Micro可以为通过ActionMessage调用的任何操作自动执行协同路由。但是,有时您可能希望直接利用协同程序特性。要执行协同程序,可以使用静态的coroutine.BeginExecute方法。

我希望这能为IResult提供一些解释和创造性的想法。请务必查看随附的示例应用程序。还有其他一些有趣的事情。

02

最后

原文标题:Caliburn.Micro Xaml made easy

原文链接:https://caliburnmicro.com/documentation/coroutines

翻译:dotnet编程大全

0 人点赞