65.9K
CodeProject 正在变化。 阅读更多。
Home

用于观察进度的可重用 ProgressViewModel (MVVM)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (9投票s)

2012年1月30日

CPOL

8分钟阅读

viewsIcon

43666

downloadIcon

760

UI和代码隐藏在不同的线程中执行。耗时操作需要异步执行。本文介绍了一种在ViewModel方法中使用此方法的方法。

引言

WPF和任务并行库的引入使得使用C#的生活比以往任何时候都轻松。是时候告别BackgroundWorker、UI冻结、事件以及所有那些完成工作但价格不菲的东西了,如果您想以有竞争力的价格开发易于使用、响应迅速、可靠的专业应用程序。

在本文中,我将展示一个ViewModel,该ViewModel可用于启动异步进程并将其进度观察到完成,通过WPF绑定。我将此模式命名为ProgressViewModel,因为它位于视图和模型之间,并且可以减轻UI设计者和开发者的负担,因为他们都有一个(但仍然灵活)接口用于将后台处理与吸引人的GUI连接起来。

这是一个稳定的解决方案,可与C# 4.0及更高版本一起使用,我们不必等到C# 5.0(或尚未必须使用Async CTP)即可实现后台处理,而**无需**担心通信/线程同步/线程上下文等问题。

假定您已经了解C#,并且对WPF、任务库和XAML有实际的了解。

StyleCop

我在项目中使用StyleCop使代码以统一的方式可读。因此,您可以下载并安装StyleCop,或者在每个.csproj文件中编辑/删除相应的条目(如果您在编译项目时遇到错误)。

<Import Project="$(ProgramFiles)\MSBuild\StyleCop\v4.5\StyleCop.Targets" />

背景

我很快在开始使用WPF时遇到了一些障碍。有时GUI会冻结,有时异常会飞出,让我困惑了一段时间。这又是一个尝试提供稳定的软件设计,以便在WPF开发耗时操作时能够轻松应对这些初始问题并立即上手。

演示应用程序

演示应用程序是一个扫描目录以查找HTML文件的程序,并使用HTML Agility Pack解析并显示每个文件的标题。

MainWindow.PNG

本文系列分为两部分。您正在阅读第一部分。在本部分中,我们将介绍一个应用程序,该应用程序接受一个文件系统文件夹的路径,其中存储着HTML文件(您可以从Wikipedia(865.75 KB)下载附加的示例数据,如果没有HTML文件,则将应用程序指向未压缩的目录。).

无论如何,将Path指向存储HTML文件的位置,然后单击Process HTML按钮。在处理您的请求时,请享受丰富的进度反馈 :-)

Using the Code

重用代码

通过将Progress DLL包含在另一个项目中并实现ProgressVM类的派生版本(在该项目中,即ProcessHTMLVM),可以重用此项目。但让我们先概述一下它是如何工作的,然后再让它为我们工作...

软件设计

在我的WPF代码中,我现在使用分层架构,其中将ApplicationViewModel附加到MainWindowDataContext,并将所有其他ViewModel放在该ViewModel下方(将其他ViewModel暴露为ApplicationViewModel的属性和方法)。让我们看一下类设计的概述,并进一步解释下面的调用顺序。

SoftwareDesign.PNG

左侧的图表为我们提供了ProgressViewModel架构的鸟瞰图。

核心是ProgressVM类(它位于单独的DLL中,以便重用可以简单地通过引用它来完成)。ProgressVM类公开了可用于控制和查看进度控件(ProgressMinProgressMaxProgressValue)中的进度,以及是否

  • 应启用开始和停止按钮(CanRunProcess)。
  • 应显示进度(IsProgressVisible)。
  • 是否应启用取消按钮(CanCancelRunProcess)。

执行主要工作的方法是RunProcess。该方法通过从ApplicationViewModel传递下来的Func<>语句来异步启动进程。

但是,ApplicationViewModel(AppVM)和ProcessVM没有直接连接。连接元素是继承自ProcessVMProcessHTMLVM类。ProcessHTMLVM作为名为mProcessing的字段存在于AppVM中,并公开了一个名为Processing的属性。

因此,当我们绑定到ProcessHTMLVM中的ProgressViewModel属性时,我们通过MainWindow->DataContext(请参阅MainWindow构造函数)和AppVM类中的Processing属性进行绑定。

ProgressResult类是一个容器类,用于保存从长时间处理任务获得的(或指向它的)结果。我们将在下面进一步讨论。

调用顺序

以下XAML代码定义了MainWindow类中的Process HTML按钮。

<Button Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"
            Command="cmd:Main.ProcessHTML"
            CommandParameter="{Binding Path=Path}"
            VerticalAlignment="Center" Grid.Row="2" Margin="3,6,3,6" /> 

Command部分将按钮绑定到View/Main.cs中静态Main类中定义的ProcessHTML命令。它告诉WPF,每当用户单击此控件时,就使用AppVM公开的Path属性的内容来执行此命令。AppVMPath属性包含当前目录路径,因为它绑定到Path文本框的Text属性。

<TextBox Text="{Binding Path=Path,UpdateSourceTrigger=PropertyChanged}" 
              HorizontalAlignment="Stretch" VerticalAlignment="Center" 
              Grid.Column="1" Grid.Row="1"/> 

现在,输入来自这里,但让我们继续看看命令是如何使用该参数调用的。通过ProcessHTML命令实际执行的方法在MainWindow类代码隐藏文件中的BindCommands方法中声明。

CommandBindings.Add(new CommandBinding(Main.ProcessHTML,
            (s, e) => this.AppVM.ProcessHTMLDirectory(e,
                      this.prgProgress,
                      this.txtProgressStatus,
                      null, this.lstStatus, null),
            (s, e) => this.AppVM.CanExecute_IfNoProcessRuns(s, e)));

我知道乍一看这看起来很糟糕,但它只是另一种将(待执行)方法绑定到命令的方法。我们使用Lambda表达式CommandBindings类来告诉WPF它应该执行

  • CanExecute_IfNoProcessRuns:确定此命令是否启用,并执行
  • ProcessHTMLDirectory方法以执行此命令。

AppVM类中的ProcessHTMLDirectory方法被调用,并带有许多控件作为参数(ProgressBar等),以便在运行时绑定ViewModel。我最初使用此方法是因为我每次启动新进程时都会销毁ProcessHTMLVM对象,并且我认为我需要动态重新绑定(通过代码)。

事实证明,XAML中的绑定就足够了,因为当Processing属性更改时,WPF会正确重新绑定。因此,我可以删除XAML中的属性绑定(因为代码隐藏中有绑定),或者从ProcessHTMLDirectory方法中删除对BindStatusBarItems的调用(这可能更符合我的做法)。

但我选择保持原样,以便人们可以看到代码隐藏和XAML中等效的绑定表达式。

无论如何,ProcessHTMLDirectory方法设置

  • BindStatusBarItems方法中创建一个新的ProgressVM类型对象(或更具体地说是ProcessHTMLVM),
  • 注册一个回调方法ProcessHTML_Results,该方法在处理完成时(无论是否有错误)执行,
  • 调用SetProgressVisibility将进度控件的状态从Collapsed切换到Visible
  • 将路径字符串放入Dictionary对象中,并将其传递给ProcessHTMLVM类中的RunProcess方法。

我正在使用Dictionary方法来传递参数和获取结果。因为,我认为这足够简单,而无需更改RunProcess方法的签名。这样,我就可以一遍又一遍地应用相同的实例化异步ProgressViewModel框架的方法,同时为每个实现执行完全不同的操作...

RunProcess方法调用的一个重要参数是ProcessHTMLDirectory部分。这实际上是一个传递给RunProcess的方法。RunProcess使用此方法通过Func<> 委托方法进行异步处理。RunProcess中的代码显示它

  • 通过其工厂启动一个Task类。
  • 创建一个新的CancelToken,以便执行过程知道何时用户想要取消正在运行的进程,并且
  • 通过execFunc(callParameters)启动实际处理(ProcessHTMLVM.ProcessHTMLDirectory)。

现在查看ProcessHTMLVM.ProcessHTMLDirectory,我们可以看到如何解压Dictionary调用参数并在其中实现HTML处理。在该方法中设置诸如this.ProgressMax之类的值,-通过AppVM和绑定-报告回UI的进度控件。

ProcessHTMLVM.ProcessHTMLDirectory方法的返回值通过类底部的this.callResults.Add语句传回AppVM中的ProcessHTML_Results

ProcessHTMLVM类定义了用于参数和结果DictionarykeyString值,以及要执行的ProcessHTMLDirectory方法。如果我们需要显示正在运行的后台进程的描述性名称,则可以使用ProcessName属性向用户输出有意义的字符串。所有其他内容均继承自ProgressVM

摘要

概述上述执行方法的顺序,我们可以看到命令在MainWindow中启动,在AppVM中转换为方法调用,在ProcessHTMLVM中处理,然后传回AppVM中的ProcessHTML_Results

同样值得注意的是Cancel命令,它在AppVM类中执行CancelProcessing方法。该调用被传递给(如果存在)ProgressVM对象,在该对象内部,CancelToken被设置为向正在运行的进程发出信号:离开这里!

关注点

我在编写一个处理大约20个命令的应用程序时发现了这种模式,总是使用相同的模板,我很快发现编写响应式和可靠的应用程序既容易又轻松。我希望它可以帮助其他人专注于他们的任务,而不必担心所有这些微小的技术和Progress DLL中的技巧。

本文的第二部分将进一步探讨此处展示的思想。在处理复杂算法时,我们将公开用于查看多个进度值的属性。

请评论并投票,让我知道您对此的看法。

历史

  • 2012年1月20日:初次创建。
© . All rights reserved.