5

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. :)

Yael
  • 1,566
  • 3
  • 18
  • 25
Rune Jacobsen
  • 9,907
  • 11
  • 58
  • 75
  • Your "test" class is probably being run in a thread by some "test harness/runner application"...that thread doesn't have a synchronizationcontext...so you tried to create one. Your thread doesn't have a messageloop though to do the synchronization. So either you need to try and get the SynchronizationContext of the "main" UI thread of the runner (if that is possible)...or if not you might be able to get your "test" run on an STA thread or change it to be STA e.g. http://www.nunit.org/index.php?p=requiresSTA&r=2.5 – Colin Smith Aug 07 '14 at 09:48
  • colinsmith: Thanks, your comment seems to make sense, but ref. with Paul Betts' answer below - this should be possible without doing that, I believe (but could be wrong). – Rune Jacobsen Aug 07 '14 at 16:37

1 Answers1

5

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.

Ana Betts
  • 73,868
  • 16
  • 141
  • 209
  • 1
    Hi Paul - thanks for replying! I tried to make the changes you suggest, but something I thought was irrelevant and left out came back to bite me - the SynchronizationContext was set because the GetLoadedFileViewModel() method needs to wait for a MessageBus message to know that this viewmodel has finished processing messages. Is there a way to have this work as part of the test? I am trying to find a way to avoid the MessageBus part, but it has eluded me thus far. I will add the GetLoadedFileViewModel method code to the question above. – Rune Jacobsen Aug 07 '14 at 16:35
  • 1
    I would try to model GetLoadedFileViewModel as an IObservable itself, so that in your test runner, you can inject a dummy version that maybe just waits 'n' seconds then finishes (or maybe, fails with an error) – Ana Betts Aug 07 '14 at 21:14
  • 1
    This is turning out to be harder than imagined - the MessageBus stuff is there because FileViewModel actually starts another thread to load data - to avoid doing this on the UI thread of course. I am looking into ways to observe this instead of waiting for the MessageBus message, but this is headache material. Will report back. – Rune Jacobsen Aug 08 '14 at 07:58
  • 2
    FileViewModel could *also* model loading the data on a background thread as an IObservable, no? Anything that you could possibly imagine as having a callback or a "Done" event, could be better expressed as an IObservable. Also, using Rx means that in your test runner, FileViewModel doesn't start a thread, it just runs synchronously, which means your tests are predictable – Ana Betts Aug 08 '14 at 08:43
  • I think what you're suggesting here is the way I will need to go. I guess I need to ramp up my thinking and "go reactive" 100% a little sooner than I thought. Restructuring an old brain is hard. ;) Thanks! – Rune Jacobsen Aug 08 '14 at 09:14
  • 2
    Remember, you can always convert Tasks into Observables, via `ToObservable`, and vice versa via `ToTask` – Ana Betts Aug 08 '14 at 16:36