Skip to content

Instantly share code, notes, and snippets.

@RolandPheasant
Last active June 20, 2022 15:29
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RolandPheasant/3bba5e32f2eefb70c538ce4acabf17cb to your computer and use it in GitHub Desktop.
Save RolandPheasant/3bba5e32f2eefb70c538ce4acabf17cb to your computer and use it in GitHub Desktop.
Extension for a transform with an inline update
public static class DynamicDataEx
{
/// <summary>
/// Transforms the items, and when an update is received, allows the preservation of the previous view model
/// </summary>
/// <typeparam name="TObject">The type of the object.</typeparam>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TDestination">The type of the destination.</typeparam>
/// <param name="source">The source.</param>
/// <param name="transformFactory">The transform factory.</param>
/// <param name="updateAction">Apply changes to the original. Example (previousTransformedItem, newOriginalItem) => previousTransformedItem.Value = newOriginalItem </param>
/// <returns></returns>
public static IObservable<IChangeSet<TDestination, TKey>> TransformWithInlineUpdate<TObject, TKey, TDestination>(this IObservable<IChangeSet<TObject, TKey>> source,
Func<TObject, TDestination> transformFactory,
Action<TDestination, TObject> updateAction = null)
{
return source.Scan((ChangeAwareCache<TDestination, TKey>)null, (cache, changes) =>
{
if (cache == null)
cache = new ChangeAwareCache<TDestination, TKey>(changes.Count);
foreach (var change in changes)
{
switch (change.Reason)
{
case ChangeReason.Add:
cache.AddOrUpdate(transformFactory(change.Current), change.Key);
break;
case ChangeReason.Update:
{
if (updateAction == null) continue;
var previous = cache.Lookup(change.Key)
.ValueOrThrow(()=> new MissingKeyException($"{change.Key} is not found."));
updateAction(previous, change.Current);
//send a refresh as this will force downstream operators
cache.Refresh(change.Key);
}
break;
case ChangeReason.Remove:
cache.Remove(change.Key);
break;
case ChangeReason.Refresh:
cache.Refresh(change.Key);
break;
case ChangeReason.Moved:
//Do nothing !
break;
}
}
return cache;
}).Select(cache => cache.CaptureChanges());
}
}
@adamradocz
Copy link

It'd be awesome if you added this to the library.

@RolandPheasant
Copy link
Author

It's next on my list reactivemarbles/DynamicData#323

@adamradocz
Copy link

adamradocz commented Jun 20, 2022

The TransformWithInlineUpdate doesn't trigger the property's notification event, so the binded property on the UI won't refresh.

internal class MainWindowViewModel : ReactiveObject, IDisposable
{
    private readonly SourceCache<TrackDto, int> _tracksCache = new(track => track.Id);
    private readonly IDisposable _cleanup;
    private readonly ReadOnlyObservableCollection<TrackViewModel> _tracks;
    private bool _disposedValue;

    public ReadOnlyObservableCollection<TrackViewModel> InboundTracks => _tracks;

    public MainWindowViewModel()
    {
        var tracksViewModelCache = _tracksCache
            .Connect()
            .TransformWithInlineUpdate(trackDto => new TrackViewModel(trackDto), (existingTrack, updateTrackDto) => existingTrack.Update(updateTrackDto));

        var tracksSourceListCleanup = tracksViewModelCache
            .AutoRefresh()
            .Sort(SortExpressionComparer<TrackViewModel>.Ascending(track => track.Id))
            .ObserveOn(new DispatcherScheduler(Application.Current.Dispatcher))
            .Bind(out _tracks)
            .DisposeMany()
            .Subscribe((_) => Console.WriteLine(_tracksCache.Count));                

        var expiredManagerCleanup = tracksViewModelCache
            .WhenPropertyChanged(track => track.State)
            .Subscribe(x =>
            {
                if (x.Value is Models.State.Expired)
                {
                    _tracksCache.RemoveKey(x.Sender.Id);
                }
            });

        var generator = new GeneratorService();
        var generatorCleanup = generator.Tracks
            .Subscribe(trackDtos => _tracksCache.Edit(innerTracks =>
            {
                foreach (var trackDto in trackDtos)
{
                    if (trackDto.Time >= GetFutureToleranceTime() || trackDto.Time <= GetExpiredToleranceTime())
                    {
                        continue;
                    }

                    innerTracks.AddOrUpdate(trackDto);
                }
            }));

        _cleanup = new CompositeDisposable(tracksSourceListCleanup, expiredManagerCleanup, generatorCleanup);
    }

    private static DateTime GetFutureToleranceTime() => DateTime.Now + TimeSpan.FromSeconds(2);
    private static DateTime GetExpiredToleranceTime() => DateTime.Now - TimeSpan.FromSeconds(10);

    #region IDispoable
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                _cleanup.Dispose();
            }

            _disposedValue = true;
        }
    }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
    #endregion
}

@RolandPheasant
Copy link
Author

That will be accommodated for when I port the operator into the library.

The existing transform operator by default also does not respond to a refresh but, has a "transformOnRefresh" flag which the user can opt into.

For now, you can simply add an extra line of code to the snippet.

 case ChangeReason.Update:
{

should become

 case ChangeReason.Refresh:
 case ChangeReason.Update:
{

and it will do as you expect

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment