Skip to content

Joker.MVVM

Tomas Fabian edited this page May 15, 2020 · 13 revisions

Reactive view models for data changes

Install:

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

Example (C#/Xaml):

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

Cloning of models

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

Updating

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

Notifications

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
  }

Ignoring stale data updates based on IVerson:

    protected virtual bool ShouldIgnoreUpdate(TModel currentModel, TModel receivedModel)
    {
      return currentModel.Timestamp >= receivedModel.Timestamp;
    }

Selection changed

Subscribing to selection changed repeats last observed change:

      ClassUnderTest.SelectionChanged.Subscribe(selectionChangedEventArgs =>
      {
        var oldValue = selectionChangedEventArgs.OldValue;
        var newValue = selectionChangedEventArgs.NewValue;
      });

How to disable adding of not found entities for ChangeType.Update notifications:

    protected override bool CanAddMissingEntityOnUpdate(TModel model)
    {
      return false;
    }

UI bulk refresh settings:

    protected virtual TimeSpan DataChangesBufferTimeSpan => TimeSpan.FromMilliseconds(250);

    protected virtual int DataChangesBufferCount => 100;

Sorting (v. 1.1)

    protected override Sort<ProductViewModel>[] OnCreateSortDescriptions()
    {
      var sortByName = new Sort<ProductViewModel>(c => c.Name, ListSortDirection.Descending);

      return new [] {sortByName};
    }

Filtering (v. 1.2)

    protected override Func<Product, bool> OnCreateModelsFilter()
    {
      return product => product.Id != 3;
    }