Skip to main content

在MVVM项目中使用async/await - 数据绑定

分类:  WPF开发 标签:  #WPF #异步编程 #MVVM 发布于: 2025-02-17 17:46:19

今天找到了几篇非常好的文章,这些文章集中讨论了MVVM模式的编程,一一分享给大家。

本篇讨论在WPF & Net MAUI & WinUE3 MVVM项目中使用Async & await进行多任务编程,是基于大牛Stephen Cleary于2014年3月份左右发表的博客。原始页面已经找不到了,进入了微软MSDN杂志的存档了。

之前在网络上搜索了不少文章,这些文章讨论如何在UI环境中使用多线程时无一例外的都是利用例如wpfdispatcher将长时间运行的线程放置到后台线程中,并通过Dispatcher来更新UI, UI线程无需等待。看到这些代码我都有一个问题,为什么不可以直接使用async/await而避免使用dispatcher来配合多任务编程呢?找了不少方案,自己也尝试设计一些思路,但都不理想,一直到我看到了Stephen的文章,顿时感觉茅塞顿开,在这里将Stephen的经验也分享给大家。

首先需要说明的基本原则:在考虑开始使用多任务编程之前,先要认真考虑一下自己的应用中,哪些是和UI线程绑定比较紧密的,哪些组件不是。在MVVM设计模式中,ViewMode由于是需要通过属性绑定到控件上,因此ViewMode组件肯定是和UI线程绑定紧密的,但是Model组件不一定,如果使用绑定将ModelViewModel连接在一起,那么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();
}
}
}

尝试编译运行一下这部分代码,可以仔细的观察一下结果以及设计的目的,希望对你有帮助。