Skip to main content

在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环境中使用多线程时无一例外的都是利用例如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();
        }
    }
}

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