2014-08-07 2 views
5

У меня есть приложение .Net 4.5, которое перемещается в WPX на основе RxUI (обновляется до версии 6.0.3 на момент написания этой статьи). У меня есть текстовое поле, которое должно функционировать как поле фильтра с довольно распространенным дросселем и т. Д., Что было частью причины перехода в первую очередь.ReactiveUI против ICollectionView

Это подходящая часть моего класса.

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 блок тестирует это с помощью 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"); 
    }); 
} 

Я поставил отладочный на Subscribe действия для моего FilterText конфигурации в конструкторе класса, и он вызывается один раз для каждого элемента пакета при запуске, но он никогда не будет вызван после изменения свойства FilterText.

Btw, конструктор тестового класса содержит следующую инструкцию, чтобы сделать многопоточность магия работы:

SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); 

Моя проблема заключается в основном о том, что метод Refresh() на мой взгляд, никогда не вызывается после того, как я изменить FilterText , и я не понимаю, почему нет.

Это простая проблема с моим кодом? Или это проблема с продуктом CollectionViewSource, работающим в контексте тестирования единицы, а не в контексте WPF?

Должен ли я отказаться от этой идеи и, скорее, иметь свойство ReactiveList, которое я фильтрую вручную всякий раз, когда происходит смена текста?

Примечание: это работает в приложении - FilterText запускает обновление там. Это просто не происходит в модульном тесте, что заставляет меня задаться вопросом, не делаю ли я это неправильно.

EDIT: В соответствии с запросом, вот соответствующие биты XAML - это просто простое окно с текстовым полем и datagrid.

TextBox:

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

Если что-нибудь еще актуален/нужно, дайте мне знать!

EDIT 2: Paul Betts рекомендует не выполнять установку SynchronizationContext в конструкторе тестов, как я, возможно, по очень веским причинам. Тем не менее, я делаю это из-за того, как работает другая viewmodel (FileViewModel) - ему нужно дождаться сообщения MessageBus, чтобы узнать, что обработка пакета завершена. Это то, что я активно работаю над попытками избежать - я знаю, что MessageBus - очень удобная плохая идея. :) Но это причина для материала SyncContext. Метод, который создает тест ViewModel выглядит следующим образом:

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

Я понимаю, что это плохой дизайн, поэтому, пожалуйста, не кричите. ;) Но я беру здесь много устаревшего кода и переношу его в MVVM на основе RxUI - я не могу сделать все это и в конечном итоге создать идеальный дизайн, и именно поэтому я получаю модульные тесты на месте для всего этого так что я смогу повторить реорганизацию Rambo позже.:)

+0

Ваш «тестовый» класс, вероятно, запускается в потоке с помощью какого-либо «тестового жгута/приложения для бегунов» ... этот поток не имеет синхронизированного текста ... поэтому вы пытались его создать. Однако для вашей нити не существует messageloop, хотя для синхронизации. Поэтому либо вам нужно попробовать получить SynchronizationContext «основного» потока пользовательского интерфейса для бегуна (если это возможно) ... или если вы не сможете запустить ваш «тест» в потоке STA или изменить его быть STA, например http://www.nunit.org/index.php?p=requiresSTA&r=2.5 –

+0

colinsmith: Спасибо, ваш комментарий, кажется, имеет смысл, но ref. с ответом Пауля Бетса ниже - это должно быть возможно без этого, я считаю (но может быть и неправ). –

ответ

5

Btw, конструктор тестового класса содержит следующую инструкцию, чтобы сделать потоковую волшебную работу:

Не делай этого

Моя проблема заключается в том, что в основном Обновить() на моем представлении никогда не вызывается после изменения FilterText, и я не понимаю, почему нет.

Я считаю, что ваша проблема является закомментирована часть:

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

И эта часть:

.ObserveOnDispatcher()

При использовании TestScheduler, вы должны использовать RxApp. [MainThread/Taskpool] Планировщик для всех параметров планировщика. Здесь выше вы используете real TaskpoolScheduler и real Диспетчер. Поскольку они не находятся под TestScheduler, они не могут контролироваться TestScheduler.

Вместо этого написать:

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

и все должно работать.

+1

Привет, Пол, спасибо за ответ! Я попытался внести изменения, которые вы предлагаете, но что-то, что я считал несущественным, и ушел, вернулся, чтобы укусить меня - SynchronizationContext был установлен, потому что метод GetLoadedFileViewModel() должен дождаться сообщения MessageBus, чтобы знать, что эта модель просмотра завершила обработку сообщений , Есть ли способ, чтобы эта работа была частью теста? Я пытаюсь найти способ избежать части MessageBus, но до сих пор это ускользало от меня. Я добавлю код метода GetLoadedFileViewModel к вопросу выше. –

+1

Я бы попытался моделировать GetLoadedFileViewModel как сам IObservable, так что в вашем тестовом бегуне вы можете ввести фиктивную версию, которая, возможно, просто ждет «n» секунд, а затем закончит (или, может быть, не с ошибкой) –

+1

Это получается быть сложнее, чем предполагалось - материал MessageBus существует, потому что FileViewModel фактически запускает другой поток для загрузки данных - чтобы избежать этого в потоке пользовательского интерфейса, конечно. Я ищу способы наблюдать это, а не ждать сообщения MessageBus, но это материал головной боли. Отчитается. –

Смежные вопросы