Winui3 桌面; ObservableCollection,在 属性 变化时更新 UI?从不同线程更新

Winui3 Desktop; ObservableCollection, updating UI when property changes? Updating from different thread

这是一个小型测试应用程序,用于尝试从我的主应用程序中找出这个问题。我先贴代码。

XAML:

<Window
    x:Class="ThreadTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ThreadTest"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Border BorderBrush="Black" BorderThickness="2" Grid.Row="0"/>

        <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="0">
            <Button Click="{x:Bind ViewModel.AddPosition}" Content="Add To List" Margin="5"/>
            <TextBlock Text="{x:Bind ViewModel.OutputString, Mode=OneWay}" Margin="5"/>
            
            <ListView ItemsSource="{x:Bind ViewModel.PositionCollection, Mode=OneWay}" Margin="5">
                <ListView.HeaderTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>

                            <Border BorderBrush="BlueViolet" BorderThickness="0,0,0,1">
                                <TextBlock Text="ID" Margin="5,0,0,0" FontWeight="Bold"/>
                            </Border>
                            <Border Grid.Column="1" BorderBrush="BlueViolet" BorderThickness="0,0,0,1">
                                <TextBlock Text="Place" Margin="5,0,0,0" FontWeight="Bold"/>
                            </Border>
                        </Grid>
                    </DataTemplate>
                </ListView.HeaderTemplate>
                
                <ListView.ItemTemplate>
                    <DataTemplate x:DataType="local:PositionModel">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>

                            <TextBlock Text="{x:Bind Path=ID, Mode=OneWay}"/>
                            <TextBlock Grid.Column="1" Text="{x:Bind Path=Place, Mode=OneWay}" />
                        </Grid>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
            
        </StackPanel>        
    </Grid>
   
</Window>

视图模型(MainViewModel.cs):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.UI.Dispatching;
using Windows.UI.Core;
using Windows.ApplicationModel;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ThreadTest {
    public class MainViewModel : BindableBase, INotifyPropertyChanged {
        private String outputString = "Empty";

        public MainViewModel() {

        }

        public String OutputString {
            get { return outputString; }
            set { SetProperty(ref outputString, value); }
        }

        private Random _random = new Random();  
        private int _id = 0;
        
        private ObservableCollection<PositionModel> _positioncollection = new();
        public ObservableCollection<PositionModel> PositionCollection {
            get { return _positioncollection; }
            set { SetProperty(ref _positioncollection, value); }
        }

        public async void AddPosition() {
            Progress<PositionModel> progress = new();
            progress.ProgressChanged += Progress_ProgressChanged;

            // Increase id for each new position added.
            _id++;
            // Setup/
            var _position = new PositionModel {
                ID = _id,
                Place = _random.Next(1, 1000), // Get a random starting point.
            };

            PositionCollection.Add(_position);            
            PositionsClass positionsClass = new(ref _position, progress);

            await Task.Run(() => { positionsClass.Start(); });
        }

        private void Progress_ProgressChanged(object sender, PositionModel e) {
            // This is so I can see that the thread is actually running.
            OutputString = Convert.ToString(e.Place);
        }
    }
}

BindableBase.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;


namespace ThreadTest {
    public class BindableBase : INotifyPropertyChanged {

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null) {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        protected bool SetProperty<T>(ref T originalValue, T newValue, [CallerMemberName] string propertyName = null) {
            if (Equals(originalValue, newValue)) {
                return false;
            }

            originalValue = newValue;
            OnPropertyChanged(propertyName);

            return true;
        }

    }
}

PositionModel.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;


namespace ThreadTest {
    public class PositionModel {

        /*
        //Impliment INotifyPropertyChanged up above if using this.
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null) {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        protected bool SetProperty<T>(ref T originalValue, T newValue, [CallerMemberName] string propertyName = null) {
            if (Equals(originalValue, newValue)) {
                return false;
            }

            originalValue = newValue;
            OnPropertyChanged(propertyName);

            return true;
        }

        private int _id = 0;
        public int ID {
            get { return _id; }
            set { SetProperty(ref _id, value); }
        }

        private int _place = 0;
        public int Place {
            get { return _place; } 
            set { SetProperty(ref _place, value); }
        }
        */

        public int ID { get; set; }
        public int Place { get; set; }  
    }
}

PositionsClass.cs:

using Microsoft.UI.Dispatching;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ThreadTest {
    public class PositionsClass {
        private IProgress<PositionModel> _progress;
        private PositionModel _position;

        public PositionsClass(ref PositionModel position, IProgress<PositionModel> progress) {
            _progress = progress;
            _position = position;
        }

        public void Start() {

            StartFakePosition();
        }

        private void StartFakePosition() {

            // Just using a quick loop to keep the numbers going up.
            while (true) {
                _position.Place++;

                // Send position back.
                _progress.Report(_position);

                Thread.Sleep(100);
            }
        }
    }
}

所以基本上你会点击 'add to list' 按钮,然后创建 PositionsClass positionsClass,创建并填充 PositionModel _position,创建一个 ObservableCollection PositionCollection(绑定到 xaml 上的列表视图)然后旋转关闭 class 进入它自己的线程。 class 将获取_position 并增加其.Place,然后progress.report _position 返回主线程。

现在我想弄清楚如何获取(ObservableCollection 的)PositionCollection 来更新 lisview ui。我订阅了 progress.ProgressChanged 并更新了 OutputString 只是为了确保 class 实际上是 运行 并且递增确实有效。

我尝试了我在网上找到的各种方法,包括不同的继承 ObversableCollection 方法,none 其中有效或我误解了它们。

我认为在 PositionModel.cs 上实现 INotifyPropertyChange 本身会起作用(注释掉的代码),但这样做会引发跨线程错误。我想这是因为单独线程上的 positionsClass 正在更新 .Place 导致跨线程错误?

任何人都可以帮助解释如何让 ObservableCollection 在我上面的示例中 属性 发生变化时更新 ui 吗?谢谢!在我的主应用程序中,我将在单独的线程上更新大量属性,而不仅仅是本示例中的 2 个。这就是为什么我认为将整个模型发送回 progress.report.

会更容易

我想我已经明白了。首先,我在上面的 PositionModel.cs 代码(注释掉的部分)上启用了 INotifyProperty。然后我添加:

        private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
        public CoreDispatcher Dispatcher { get; }

到MainViewModel,修改AddPosition为:

        public async void AddPositionDispatcher() {

            // Increase id for each new position added.
            _id++;
            // Setup/
            var _position = new PositionModel {
                ID = _id,
                Place = _random.Next(1, 1000), // Get a random starting point.
            };

            PositionCollection.Add(_position);
            PositionsClassDispatcher positionsClassDispatcher = new(_position, _dispatcherQueue);

            await Task.Run(() => { positionsClassDispatcher.Start(); });
        }

我将 DispatcherQueue 发送到新修改的位置 PositionsClassDispatcher.cs:

using Microsoft.UI.Dispatching;
using System.Threading;


namespace ThreadTest {
    internal class PositionsClassDispatcher {
        private PositionModel _position;
        DispatcherQueue _queue;

        public PositionsClassDispatcher(PositionModel position, DispatcherQueue dispatcherQueue) {
            _queue = dispatcherQueue;
            _position = position;
        }

        public void Start() {
            StartFakePosition();
        }

        private void StartFakePosition() {

            // Just using a quick loop to keep the numbers going up.
            while (true) {  
              _queue.TryEnqueue(() => {
                    _position.Place++;
                });
                Thread.Sleep(100);
            }
        }
    }
}

这将使用 DispatcherQueue 并使用 TryEnqueue 来更新 _position.Place。 ObvservableCollection 现在可以在更新属性时正确更新 UI。同时更新 XAML 以使用新的 AddPositionDispatcher()。

此外,必须使用 DispatcherQueue 而不是 Dispatcher,因为 WinUI3 似乎不再有 Dispatcher。

Window.Dispatcher Property

Window.Dispatcher may be altered or unavailable in future releases. Use Window.DispatcherQueue instead.

这导致了很多问题,试图解决这个问题,因为很多信息都是基于 Dispatcher 而不是 DispatcherQueue。

希望这对遇到问题的其他人有所帮助。