C# 反应UI与ICollectionView

C# 反应UI与ICollectionView,c#,wpf,system.reactive,xunit.net,reactiveui,C#,Wpf,System.reactive,Xunit.net,Reactiveui,我有一个.NET4.5应用程序正在迁移到基于WPF的RxUI(保持最新,截至本文撰写时为6.0.3)。我有一个文本字段,它应该作为一个过滤器字段,具有相当常见的节流阀等功能。这是一开始变为被动的部分原因 这是我的课程的相关部分 public class PacketListViewModel : ReactiveObject { private readonly ReactiveList<PacketViewModel> _packets; private Packe

我有一个.NET4.5应用程序正在迁移到基于WPF的RxUI(保持最新,截至本文撰写时为6.0.3)。我有一个文本字段,它应该作为一个过滤器字段,具有相当常见的节流阀等功能。这是一开始变为被动的部分原因

这是我的课程的相关部分

public class PacketListViewModel : ReactiveObject
{
    private readonly ReactiveList<PacketViewModel> _packets;
    private PacketViewModel _selectedPacket;
    private readonly ICollectionView _packetView;
    private string _filterText;

    /// <summary>
    /// Gets the collection of packets represented by this object
    /// </summary>
    public ICollectionView Packets 
    {
        get
        {
            if (_packets.Count == 0)
                RebuildPacketCollection();
            return _packetView;
        }
    }

    public string FilterText
    {
        get { return _filterText; }
        set { this.RaiseAndSetIfChanged(ref _filterText, value); }
    }

    public PacketViewModel SelectedPacket
    {
        get { return _selectedPacket; }
        set { this.RaiseAndSetIfChanged(ref _selectedPacket, value); }
    }

    public PacketListViewModel(IEnumerable<FileViewModel> files)
    {
        _packets = new ReactiveList<PacketViewModel>();
        _packetView = CollectionViewSource.GetDefaultView(_packets);
        _packetView.Filter = PacketFilter;

        _filterText = String.Empty;

        this.WhenAnyValue(x => x.FilterText)
            .Throttle(TimeSpan.FromMilliseconds(300)/*, RxApp.TaskpoolScheduler*/)
            .DistinctUntilChanged()
            .ObserveOnDispatcher()
            .Subscribe(_ => _packetView.Refresh());
    }

    private bool PacketFilter(object item)
    {
        // Filter logic
    }

    private void RebuildPacketCollection()
    {
        // Rebuild packet list from data source
        _packetView.Refresh();
    }
}
我的问题基本上是,在我更改FilterText之后,视图上的Refresh()方法永远不会被调用,我看不出为什么不能

这是我的代码的一个简单问题吗?或者这是在单元测试上下文而不是WPF上下文中运行的CollectionViewSource的问题吗

我是否应该放弃这个想法,而是在触发文本更改时手动筛选一个ReactiveList属性

注意:这在应用程序中起作用-FilterText在那里触发更新。它只是在单元测试中没有发生,这让我怀疑我是否做错了

编辑:根据要求,这里是XAML的相关部分-目前这只是一个带有文本框和数据网格的简单窗口

文本框:

<TextBox Name="FilterTextBox"
         Grid.Column="1"
         VerticalAlignment="Center"
         Text="{Binding FilterText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
         />

数据网格:

<DataGrid ItemsSource="{Binding Path=Packets}"
          Name="PacketDataGrid"
          SelectedItem="{Binding SelectedPacket}"
          AutoGenerateColumns="False"
          EnableRowVirtualization="True"
          SelectionMode="Single"
          SelectionUnit="FullRow"
          CanUserAddRows="False"
          CanUserResizeRows="False"
          >
    <DataGrid.Columns>
...

...
如果还有其他相关/需要的信息,请告诉我

编辑2:Paul Betts建议不要像我一样在测试构造函数中进行SynchronizationContext设置,可能是出于非常合理的原因。然而,我这样做是因为另一个viewmodel(FileViewModel)的工作方式——它需要等待MessageBus消息来知道数据包处理已经完成。这是我正在积极努力避免的事情——我知道MessageBus是一个非常方便的坏主意但这就是SyncContext的原因。创建测试视图模型的方法如下所示:

private FileViewModel GetLoadedFileViewModel()
{
    var mre = new ManualResetEventSlim();
    var fvm = new FileViewModel(new MockDataLoader());
    MessageBus.Current
              .Listen<FileUpdatedPacketListMessage>(fvm.MessageToken.ToString())
              .Subscribe(msg => mre.Set());
    fvm.LoadFile("irrelevant.log");

    mre.Wait(500);

    return fvm;
}
私有文件视图模型GetLoadedFileViewModel()
{
var mre=新的手动重置事件slim();
var fvm=newfileviewmodel(newmockdataloader());
MessageBus.Current
.Listen(fvm.MessageToken.ToString())
.Subscribe(msg=>mre.Set());
fvm.LoadFile(“unrelated.log”);
mre.Wait(500);
返回fvm;
}
我意识到这是一个糟糕的设计,所以请不要大喊大叫但是我在这里使用了大量的遗留代码,并将其移植到基于RxUI的MVVM中——我还不能完成所有的工作,并最终得到一个完美的设计,这就是为什么我要对所有这些东西进行单元测试,以便我以后可以进行Rambo重构的原因。:)

顺便说一句,测试类的构造函数包含以下语句,以使线程魔术工作:

SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
不要这样做

我的问题基本上是,在我更改FilterText之后,视图上的Refresh()方法永远不会被调用,我看不出为什么不能

我相信您的问题在于被注释掉的部分:

.Throttle(时间跨度.从毫秒(300)/,RxApp.TaskpoolScheduler/)

这部分:

.ObserveOnDispatcher()

当您使用TestScheduler时,必须使用RxApp。[MainThread/Taskpool]Scheduler处理所有调度器参数。在上面,您使用的是一个真正的TaskpoolScheduler和一个真正的Dispatcher。因为它们不在TestScheduler下,所以不能由TestScheduler控制

相反,写下:

    this.WhenAnyValue(x => x.FilterText)
        .Throttle(TimeSpan.FromMilliseconds(300), RxApp.TaskpoolScheduler)
        .DistinctUntilChanged()
        .ObserveOn(RxApp.MainThreadScheduler)
        .Subscribe(_ => _packetView.Refresh());

一切都会好起来的

您的“test”类可能正在某个线程中由某个“test harness/runner应用程序”运行…该线程没有synchronizationcontext…因此您尝试创建一个。但是,您的线程没有messageloop来执行同步。因此,您需要尝试获取运行程序的“主”UI线程的SynchronizationContext(如果可能的话)……或者如果不可能,您可能可以在STA线程上运行“测试”,或者将其更改为STA。例如,colinsmith:谢谢,您的评论似乎有意义,但参考以下保罗·贝茨的答案——我相信不这样做也可以做到(但可能是错误的)。嗨,保罗——谢谢你的回答!我试图进行您建议的更改,但我认为不相关且遗漏的内容再次困扰了我-设置SynchronizationContext是因为GetLoadedFileViewModel()方法需要等待MessageBus消息才能知道此viewmodel已完成对消息的处理。有没有办法把这项工作作为测试的一部分?我试图找到一种避免使用MessageBus部分的方法,但到目前为止我一直没有找到。我将在上述问题中添加GetLoadedFileViewModel方法代码。我将尝试将GetLoadedFileViewModel建模为一个IObservable本身,以便在测试运行程序中,您可以注入一个虚拟版本,该版本可能只需等待“n”秒,然后完成(或者可能因错误而失败)事实证明,这比想象的要难——MessageBus的东西就在那里,因为FileViewModel实际上启动了另一个线程来加载数据——当然要避免在UI线程上这样做。我正在寻找观察这一点的方法,而不是等待MessageBus消息,但这是令人头痛的材料。FileViewModel也可以将数据加载到后台线程作为IObservable来建模,不是吗?任何您可以想象为具有回调或“完成”事件的内容,都可以更好地表示为IObservable。另外,使用Rx意味着在您的测试运行程序中,FileViewModel不会启动线程,它只是同步运行,这意味着您的测试是可预测的。记住,您始终可以通过
ToObservable
将任务转换为可观察的,反之亦然,通过
ToTask
    this.WhenAnyValue(x => x.FilterText)
        .Throttle(TimeSpan.FromMilliseconds(300), RxApp.TaskpoolScheduler)
        .DistinctUntilChanged()
        .ObserveOn(RxApp.MainThreadScheduler)
        .Subscribe(_ => _packetView.Refresh());