在MVVM项目中使用async/await - 数据绑定
分类: .Net技术 ◆ 标签: #异步编程 #.Net #MVVM ◆ 发布于: 2023-08-07 21:30:33

今天找到了几篇非常好的文章,这些文章集中讨论了MVVM
模式的编程,一一分享给大家。
本篇讨论在WPF & Net MAUI & WinUE3 MVVM
项目中使用Async & await
进行多任务编程,是基于大牛Stephen Cleary
于2014年3月份左右发表的博客。原始页面已经找不到了,进入了微软MSDN
杂志的存档了。
之前在网络上搜索了不少文章,这些文章讨论如何在UI环境中使用多线程时无一例外的都是利用例如wpf
的dispatcher
将长时间运行的线程放置到后台线程中,并通过Dispatcher
来更新UI, UI线程无需等待。看到这些代码我都有一个问题,为什么不可以直接使用async/await
而避免使用dispatcher
来配合多任务编程呢?找了不少方案,自己也尝试设计一些思路,但都不理想,一直到我看到了Stephen
的文章,顿时感觉茅塞顿开,在这里将Stephen
的经验也分享给大家。
首先需要说明的基本原则:在考虑开始使用多任务编程之前,先要认真考虑一下自己的应用中,哪些是和UI线程绑定比较紧密的,哪些组件不是。在MVVM
设计模式中,ViewMode
由于是需要通过属性绑定到控件上,因此ViewMode
组件肯定是和UI线程绑定紧密的,但是Model
组件不一定,如果使用绑定将Model
和ViewModel
连接在一起,那么Model
就是和UI线程绑定紧密,反之则不是。
还需要注意的是多任务编程的场景,在基于wpf & Net MAUI & WinUE3
的编程中,多任务场景大多数在访问外部资源上。
为了说明整个思路,我们设计一个非常简单的wpf
应用:
创建一个wpf
应用,在窗体上放置一个标签,该标签用来显示后台任务抓取一个网页的总字节大小。
窗体的XAML
如下:
<Window x:Class="AsyncMVVMDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:AsyncMVVMDemo" mc:Ignorable="d" Title="MainWindow" Height="120" Width="300" WindowStartupLocation="CenterScreen"> <Window.Resources> <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> </Window.Resources> <StackPanel> <!-- 忙碌状态 --> <Label Content="下载中..." Visibility="{Binding UrlByteCount.IsNotCompleted, Converter={StaticResource BooleanToVisibilityConverter}}" Height="105" Width="300"/> <!-- 显示结果 --> <Label Content="总的字节数:" /> <Label Content="{Binding UrlByteCount.Result}" Visibility="{Binding UrlByteCount.IsSuccessfullyCompleted, Converter={StaticResource BooleanToVisibilityConverter}}" Height="105" Width="300"/> <!-- 显示具体的错误 --> <Label Content="{Binding UrlByteCount.ErrorMessage}" Background="Red" Visibility="{Binding UrlByteCount.IsFaulted, Converter={StaticResource BooleanToVisibilityConverter}}" Height="105" Width="300"/> </StackPanel> </Window>
定义了一个简单的窗体之后,我们从上述的XAML
代码可以看到我们有一个转换需要定义:BooleanToVisibilityConverter
, 向工程里创建一个类:BooleanToVisibilityConverter.cs
, 内容如下:
namespace AsyncMVVMDemo { public class BooleanToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { bool boolToStatus = value is not null && (bool) value; if ( boolToStatus ) { return "Visible"; } else { return "Hidden"; } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
这个类的作用非常简单,只要将bool
值转化为控件属性Visibility
预定义值就好了:
Visible
: 控件可见Hidden
: 空间隐藏
我们设计好了转化器之后,就开始设计ViewModel
组件了,ViewModel
组件通过暴露属性,并通过窗口的DataContext
赋值,窗口中的控件通过绑定连接到ViewModel
的属性上。
如下是我们ViewModel
的定义:
MainViewModel.cs
:
namespace AsyncMVVMDemo { public class MainViewModel { public MainViewModel() { UrlByteCount = new NotifyTaskCompletion<int>( MyStaticService.CountBytesInUrlAsync("http://www.baidu.com")); } public NotifyTaskCompletion<int> UrlByteCount { get; private set; } } }
有过MVVM
项目经验的同仁,一样就能看到这个ViewModel
的问题:没有继承接口INotifyPropertyChanged
, 当属性发生变化时无法通知绑定更新控件显示,不过细心的人应该能够观察到我们这里有一个未定义的类:NotifyTaskCompletion<int>
(还有一个Service
类), 一切的秘密都在这个类上,如下是类NotifyTaskCompletion<TResult>
的完整定义:
NotifyTaskCompletion.cs
:
namespace AsyncMVVMDemo { public class NotifyTaskCompletion<TResult> : INotifyPropertyChanged { public NotifyTaskCompletion(Task<TResult> task) { Task = task; if (!task.IsCompleted) { var _ = WatchTaskAsync(task); } } public Task<TResult> Task { get; private set; } public TResult? Result { get { return (Task.Status == TaskStatus.RanToCompletion) ? Task.Result : default(TResult); } } public TaskStatus Status { get { return Task.Status; } } public bool IsCompleted { get { return Task.IsCompleted; } } public bool IsNotCompleted { get { return !Task.IsCompleted; } } public bool IsSuccessfullyCompleted { get { return Task.Status == TaskStatus.RanToCompletion; } } public bool IsCanceled { get { return Task.IsCanceled; } } public bool IsFaulted { get { return Task.IsFaulted; } } public AggregateException? Exception { get { return Task.Exception; } } public Exception? InnerException { get { return Exception?.InnerException; } } public string? ErrorMessage { get { return InnerException?.Message; } } public event PropertyChangedEventHandler? PropertyChanged; /** * 最重要的WatchTaskAsync */ private async Task WatchTaskAsync(Task task) { try { await task; } catch { } var propertyChanged = PropertyChanged; if (propertyChanged == null) return; propertyChanged(this, new PropertyChangedEventArgs("Status")); propertyChanged(this, new PropertyChangedEventArgs("IsCompleted")); propertyChanged(this, new PropertyChangedEventArgs("IsNotCompleted")); if (task.IsCanceled) { propertyChanged(this, new PropertyChangedEventArgs("IsCanceled")); } else if (task.IsFaulted) { propertyChanged(this, new PropertyChangedEventArgs("IsFaulted")); propertyChanged(this, new PropertyChangedEventArgs("Exception")); propertyChanged(this, new PropertyChangedEventArgs("InnerException")); propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage")); } else { propertyChanged(this, new PropertyChangedEventArgs("IsSuccessfullyCompleted")); propertyChanged(this, new PropertyChangedEventArgs("Result")); } } } }
所有的秘密都在这个类上,这个类继承了接口INotifyPropertyChanged
, 然后暴露了所有和Task
相关的属性,对比在窗口上的绑定, 你就可以明确的了解这个设计,无需Dispatcher, 只需要完整的async/await
就可以实现在UI线程之外的多任务编程。
另外上面还少了一个Service
类的定义,代码如下:
MyStaticService.cs
:
namespace AsyncMVVMDemo { public class MyStaticService { public static async Task<int> CountBytesInUrlAsync(string url) { // 为了演示特意delay 10秒. await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false); // 下载数据. using (var client = new HttpClient()) { var data = await client.GetByteArrayAsync(url).ConfigureAwait(false); return data.Length; } } } }
这个服务包含一个静态的异步方法。
剩下只需要在窗口代码中指定DataContext
如下:
namespace AsyncMVVMDemo { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { DataContext = new MainViewModel(); InitializeComponent(); } } }
尝试编译运行一下这部分代码,可以仔细的观察一下结果以及设计的目的,希望对你有帮助。