Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactiveUI vs. ICollectionView

I have a .Net 4.5 app that is moving to WPF-based RxUI (kept up to date, 6.0.3 as of this writing). I have a text field that should function as a filter field with the fairly common throttle etc. stuff that was part of the reason for going reactive in the first place.

Here is the relevant part of my class.

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();
    }
}

I unit test this using Xunit.net with Resharper's test runner. I create some test data and run this test:

[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");
    });
}

I put a debug statement on the Subscribe action for my FilterText config in the constructor of the class, and it gets called once for each packet item at startup, but it never gets called after I change the FilterText property.

Btw, the constructor for the test class contains the following statement to make threading magic work:

SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());

My problem is basically that the Refresh() method on my view never gets called after I change the FilterText, and I can't see why not.

Is this a simple problem with my code? Or is this a problem with a CollectionViewSource thing running in a unit testing context rather than in a WPF context?

Should I abandon this idea and rather have a ReactiveList property that I filter manually whenever a text change is triggered?

Note: This works in the application - the FilterText triggers the update there. It just doesn't happen in the unit test, which makes me wonder whether I am doing it wrong.

EDIT: As requested, here are the relevant bits of XAML - this is for now just a simple window with a textbox and a datagrid.

The TextBox:

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

The 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>
...

If anything else is relevant/needed, let me know!

EDIT 2: Paul Betts recommends not doing the SynchronizationContext setup in the test constructor like I do, probably for very valid reasons. However, I do this because of the way another viewmodel (FileViewModel) works - it needs to wait for a MessageBus message to know that packet processing is complete. This is something that I am working actively on trying to avoid - I know the MessageBus is a very convenient bad idea. :) But this is the cause for the SyncContext stuff. The method that creates the test viewmodel looks like this:

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;
}

I realize this is bad design, so please don't yell. ;) But I am taking a lot of legacy code here and moving it into RxUI based MVVM - I can't do it all and end up with a perfect design just yet, which is why I am getting unit tests in place for all this stuff so that I can do Rambo refactoring later. :)

like image 546
Rune Jacobsen Avatar asked Aug 07 '14 08:08

Rune Jacobsen


1 Answers

Btw, the constructor for the test class contains the following statement to make threading magic work:

Don't do this

My problem is basically that the Refresh() method on my view never gets called after I change the FilterText, and I can't see why not.

I believe your problem is the commented out part:

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

And this part:

.ObserveOnDispatcher()

When you use TestScheduler, you must use RxApp.[MainThread/Taskpool]Scheduler for all scheduler parameters. Here above, you're using a real TaskpoolScheduler and a real Dispatcher. Since they're not under TestScheduler, they can't be controlled by TestScheduler.

Instead, write:

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

and everything should work.

like image 198
Ana Betts Avatar answered Oct 05 '22 23:10

Ana Betts