ReactiveUI与ICollectionView

时间:2014-08-07 08:58:34

标签: c# wpf system.reactive xunit.net reactiveui

我有一个.Net 4.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();
    }
}

我使用Xunit.net和Resharper的测试运行器进行单元测试。我创建了一些测试数据并运行此测试:

[Fact]
public void FilterText_WhenThrottleTimeoutHasPassed_FiltersProperly()
{
    new TestScheduler().With(s =>
    {
        // Arrange
        var fvm = GetLoadedFileViewModel();
        var sut = new PacketListViewModel(fvm);
        var lazy = sut.Packets;

        // Act
        sut.FilterText = "Call";
        s.AdvanceToMs(301);

        // Assert
        var res = sut.Packets.OfType<PacketViewModel>().ToList();
        sut.Packets.OfType<PacketViewModel>()
           .Count().Should().Be(1, "only a single packet should match the filter");
    });
}

我在类的构造函数中为我的FilterText配置的Subscribe操作添加了一个debug语句,并且在启动时为每个数据包项调用一次,但在更改FilterText属性后它永远不会被调用。

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

SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());

我的问题基本上是在我更改FilterText后,我的视图上的Refresh()方法永远不会被调用,我不明白为什么不这样做。

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

我应该放弃这个想法,而是拥有一个ReactiveList属性,只要触发文本更改,我就会手动过滤它?

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

编辑:根据要求,以下是XAML的相关内容 - 现在只是一个带有文本框和数据网格的简单窗口。

TextBox:

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

datagrid:

<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设置,可能是出于非常正确的原因。但是,我这样做是因为另一个视图模型(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;
}

我意识到这是糟糕的设计,所以请不要大喊大叫。 ;)但是我在这里采用了很多遗留代码并将其转移到基于RxUI的MVVM中 - 我无法做到这一切并最终得到一个完美的设计,这就是我为什么要进行单元测试的原因所有这些东西,以便我以后可以做Rambo重构。 :)

1 个答案:

答案 0 :(得分:5)

  

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

不要这样做

  

我的问题基本上是在我更改FilterText后,我的视图上的Refresh()方法永远不会被调用,我不知道为什么不这样做。

我相信你的问题是注释掉的部分:

  

.Throttle(TimeSpan.FromMilliseconds(300)/ ,RxApp.TaskpoolScheduler /)

这一部分:

  

.ObserveOnDispatcher()

当您使用TestScheduler时,必须使用RxApp。[MainThread / Taskpool]调度程序用于所有调度程序参数。在上面,您使用的是真正的 TaskpoolScheduler和真正的调度程序。由于它们不在TestScheduler下,因此它们无法由TestScheduler控制。

相反,写一下:

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

一切都应该有效。