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






4.94/5 (9投票s)
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解析并显示每个文件的标题。
本文系列分为两部分。您正在阅读第一部分。在本部分中,我们将介绍一个应用程序,该应用程序接受一个文件系统文件夹的路径,其中存储着HTML文件(您可以从Wikipedia(865.75 KB)下载附加的示例数据,如果没有HTML文件,则将应用程序指向未压缩的目录。).
无论如何,将Path指向存储HTML文件的位置,然后单击Process HTML按钮。在处理您的请求时,请享受丰富的进度反馈 :-)
Using the Code
重用代码
通过将Progress DLL包含在另一个项目中并实现ProgressVM
类的派生版本(在该项目中,即ProcessHTMLVM
),可以重用此项目。但让我们先概述一下它是如何工作的,然后再让它为我们工作...
软件设计
在我的WPF代码中,我现在使用分层架构,其中将ApplicationViewModel附加到MainWindow
的DataContext
,并将所有其他ViewModel放在该ViewModel下方(将其他ViewModel暴露为ApplicationViewModel的属性和方法)。让我们看一下类设计的概述,并进一步解释下面的调用顺序。
![]() |
左侧的图表为我们提供了ProgressViewModel架构的鸟瞰图。 核心是
执行主要工作的方法是 但是,ApplicationViewModel( 因此,当我们绑定到
|
调用顺序
以下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
属性的内容来执行此命令。AppVM
的Path
属性包含当前目录路径,因为它绑定到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
类定义了用于参数和结果Dictionary
的keyString
值,以及要执行的ProcessHTMLDirectory
方法。如果我们需要显示正在运行的后台进程的描述性名称,则可以使用ProcessName
属性向用户输出有意义的字符串。所有其他内容均继承自ProgressVM
。
摘要
概述上述执行方法的顺序,我们可以看到命令在MainWindow
中启动,在AppVM
中转换为方法调用,在ProcessHTMLVM
中处理,然后传回AppVM
中的ProcessHTML_Results
。
同样值得注意的是Cancel
命令,它在AppVM
类中执行CancelProcessing
方法。该调用被传递给(如果存在)ProgressVM
对象,在该对象内部,CancelToken
被设置为向正在运行的进程发出信号:离开这里!
关注点
我在编写一个处理大约20个命令的应用程序时发现了这种模式,总是使用相同的模板,我很快发现编写响应式和可靠的应用程序既容易又轻松。我希望它可以帮助其他人专注于他们的任务,而不必担心所有这些微小的技术和Progress DLL中的技巧。
本文的第二部分将进一步探讨此处展示的思想。在处理复杂算法时,我们将公开用于查看多个进度值的属性。
请评论并投票,让我知道您对此的看法。
历史
- 2012年1月20日:初次创建。