-
Notifications
You must be signed in to change notification settings - Fork 23
Joker.MVVM
Reactive view models for data changes
https://www.nuget.org/packages/Joker.MVVM/
Install-Package Joker.MVVM
ReactiveListViewModel subscribes to data change notifications and pre-fetches data from Query. Notifications are buffered until the query finishes. In order to prevent data races, POCOs are marked with Timestamp version.
See also code samples
using var reactiveProductsViewModel =
new ReactiveProductsViewModel(new SampleDbContext(connectionString), new ReactiveData<Product>(), new WpfSchedulersFactory());
reactiveProductsViewModel.SubscribeToDataChanges();
<BusyIndicator IsBusy="{Binding IsLoading}" Content="Loading">
<ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</BusyIndicator>
public class ReactiveProductsViewModel : ReactiveListViewModel<Product, ProductViewModel>
{
private readonly ISampleDbContext sampleDbContext;
private readonly IWpfSchedulersFactory schedulersFactory;
public ReactiveProductsViewModel(
ISampleDbContext sampleDbContext,
IReactiveData<Product> reactive,
IWpfSchedulersFactory schedulersFactory)
: base(reactive, schedulersFactory)
{
this.sampleDbContext = sampleDbContext;
this.schedulersFactory = schedulersFactory ?? throw new ArgumentNullException(nameof(schedulersFactory));
Comparer = new DomainEntityComparer();
}
protected override IScheduler DispatcherScheduler => schedulersFactory.Dispatcher;
protected override IObservable<IEnumerable<Product>> Query
{
get
{
return Observable.Start(() => sampleDbContext.Products.ToList(), schedulersFactory.ThreadPool);
}
}
protected override IEqualityComparer<Product> Comparer { get; }
protected override IComparable GetId(Product model)
{
return model.Id;
}
protected override ProductViewModel CreateViewModel(Product model)
{
return new ProductViewModel(model);
}
protected override Action<Product, ProductViewModel> UpdateViewModel()
{
return (model, viewModel) => viewModel.UpdateFrom(model);
}
protected override Product GetModel(EntityChange<Product> entityChange)
{
return entityChange.Entity.Clone();
}
protected override Sort<ProductViewModel>[] OnCreateSortDescriptions()
{
var sortByName = new Sort<ProductViewModel>(c => c.Name, ListSortDirection.Descending);
return new [] {sortByName};
}
}
Mark your POCO as Serializable, use AutoMapper or add a clone method:
[Serializable]
public class TestModel : IVersion
{
public int Id { get; set; }
public DateTime Timestamp { get; set; }
public string Name { get; set; }
public TestModel Clone()
{
return MemberwiseClone() as TestModel;
}
}
Override ReactiveListViewModel<,>.GetModel:
protected override TestModel GetModel(EntityChange<TestModel> entityChange)
{
var entity = entityChange.Entity?.Clone();
return entity;
}
Cloning of received models help you to avoid subtle bugs with sharing the same model in multiple viewmodel instances. Example of using the original model:
protected override TestModel GetModel(EntityChange<TestModel> entityChange)
{
return entityChange.Entity;
}
During updates the old view model is removed and new one is created instead of it. You can override this behavior providing an update method in ReactiveListViewModel<,>.UpdateViewModel:
protected override Action<TestModel, TestViewModel> UpdateViewModel()
{
return (model, viewModel) => viewModel.UpdateFrom(model);
}
ReactiveListViewModel reacts to Create, update or delete notifications:
public interface IReactiveData<TEntity>
where TEntity : IVersion
{
IObservable<EntityChange<TEntity>> WhenDataChanges { get; }
}
public class EntityChange<TEntity>
where TEntity : IVersion
{
public TEntity Entity { get; set; }
public ChangeType ChangeType { get; set; }
}
public enum ChangeType
{
Create,
Update,
Delete
}
protected virtual bool ShouldIgnoreUpdate(TModel currentModel, TModel receivedModel)
{
return currentModel.Timestamp >= receivedModel.Timestamp;
}
Subscribing to selection changed repeats last observed change:
ClassUnderTest.SelectionChanged.Subscribe(selectionChangedEventArgs =>
{
var oldValue = selectionChangedEventArgs.OldValue;
var newValue = selectionChangedEventArgs.NewValue;
});
protected override bool CanAddMissingEntityOnUpdate(TModel model)
{
return false;
}
protected virtual TimeSpan DataChangesBufferTimeSpan => TimeSpan.FromMilliseconds(250);
protected virtual int DataChangesBufferCount => 100;
protected override Sort<ProductViewModel>[] OnCreateSortDescriptions()
{
var sortByName = new Sort<ProductViewModel>(c => c.Name, ListSortDirection.Descending);
return new [] {sortByName};
}
protected override Func<Product, bool> OnCreateModelsFilter()
{
return product => product.Id != 3;
}