我目前正在使用"深度数据绑定"中推荐的测验游戏示例中的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
委托,而不是对象的消费者。现在我可以写这样的代码:
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>