扩展BindableBase为WinUI3/Uno平台的UI线程安全



我目前正在使用"深度数据绑定"中推荐的测验游戏示例中的BindableBase实现。MSDN文章。因为我需要能够从后台线程更新属性,并且不喜欢在DispatcherQueue_TryEnqueue中包装所有写入,所以我修改了BindableBase如下:

public abstract class BindableBase : INotifyPropertyChanged
{
    //cache the synchronization context on object creation, once
    private static SynchronizationContext _SynchronizationContext = SynchronizationContext.Current;
    /* ...unchanged code from original version... */
    
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        //notify all delegates on the UI thread's synchronization context
        _SynchronizationContext.Post(d => {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }, null);
    }
}

这似乎是预期的工作,与明显的警告,所有派生对象必须在UI线程上创建,这对我来说是好的。然而,我不是线程专家,所以我想知道这是否会导致意想不到的问题。我也是在发现DispatcherQueue之前写的这篇文章;在调用PropertyChanged委托时,我应该使用该同步上下文吗?

——更新

我应该澄清为什么我要这样做。通过上述更改,我使对象负责确保它总是调用UI线程上的PropertyChanged委托,而不是对象的消费者。现在我可以写这样的代码:

<<p> 绑定对象/strong>
public class BindableObject : BindableBase
{
    private int _SomeValue = 0;
    public int SomeValue
    {
        get => _SomeValue;
        set => SetProperty(ref _SomeValue, value);
    }
}

XAML

<TextBlock Text="{x:Bind MyObject.SomeValue, Mode=OneWay}"/>

MyPage.cs

public sealed partial class MyPage : Page
{
    public BindableObject MyObject { get; init; } = new();
    
    private async void Button_Click(object sender, RoutedEventArgs args)
    {
        await Task.Run(async () => {
            //do something interesting here
            
            MyObject.SomeValue = 1;
                //no call to DispatcherQueue
                //but no exceptions and x:Bind updates correctly
        });
    }
}

我在发现DispatcherQueue之前也写了这个;我应该使用过度同步上下文吗?

是的,AFAIK, DispatcherQueue是一种从非UI线程访问UI的方式。

这似乎是预期的工作,与明显的警告,所有派生对象必须在UI线程上创建,这对我来说是好的。然而,我不是线程专家,所以我想知道这是否会导致意想不到的问题。

如果可能的话,你应该先实例化你的对象,然后只使用DispatcherQueue来访问UI。

这是一个使用CommunityToolkit.Mvvm的示例代码。

MainPageViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Dispatching;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ObservableCollections;
public record HeavyItem(TimeSpan TimeSpan);
public partial class MainPageViewModel : ObservableObject
{
    private readonly DispatcherQueue dispatcherQueue;
    [ObservableProperty]
    private ObservableCollection<HeavyItem> items = new();
    public MainPageViewModel()
    {
        this.dispatcherQueue = DispatcherQueue.GetForCurrentThread();
    }
    [RelayCommand(IncludeCancelCommand = true)]
    private Task Test(CancellationToken cancellationToken)
    {
        return Task.Run(async () =>
        {
            try
            {
                var stopwatch = Stopwatch.StartNew();
                while (true)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    // First, instantiate the item,
                    HeavyItem item = new(stopwatch.Elapsed);
                    // then use the DispatcherQueue to add the item.
                    this.dispatcherQueue?.TryEnqueue(() => Items.Add(item));
                    await Task.Delay(TimeSpan.FromMilliseconds(10));
                }
            }
            catch (OperationCanceledException)
            {
            }
        }, cancellationToken);
    }
}

MainPage.xaml.cs

using Microsoft.UI.Xaml.Controls;
namespace ObservableCollections;
public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
    }
    public MainPageViewModel ViewModel { get; } = new();
}

MainPage.xaml

<Page
    x:Class="ObservableCollections.MainPage"
    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:local="using:ObservableCollections"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">
    <Grid RowDefinitions="Auto,*">
        <StackPanel
            Grid.Row="0"
            Orientation="Horizontal">
            <Button
                Command="{x:Bind ViewModel.TestCommand, Mode=OneWay}"
                Content="Start" />
            <Button
                Command="{x:Bind ViewModel.TestCancelCommand, Mode=OneWay}"
                Content="Cancel" />
        </StackPanel>
        <ScrollViewer
            Grid.Row="1"
            ScrollViewer.VerticalScrollBarVisibility="Visible"
            ScrollViewer.VerticalScrollMode="Enabled">
            <ItemsRepeater ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}" />
        </ScrollViewer>
    </Grid>
</Page>

最新更新