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. :)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With