Search code examples
c#.netsystem.reactivereactivereactiveui

How do we create an IObservableList<T> from an IObservable<IList<T>>?


Question

How do we go from an IObservable<IReadOnlyList<T>> to an IObservableList<T> (from DynamicData)?

Context

I'm using both Reactive extensions and DynamicData in my project.

I currently have a list of strings that changes over time. I have at as an IObservable<IReadOnlyList<string>>. It yields updates like the following:

  1. ["A", "C", "F"]
  2. ["A", "B", "C", "F"]
  3. ["A", "B", "C"]

I want to convert this into an IObservableList<string> that would yield updates like the following:

  1. Add "A","C","F" at index 0
  2. Add "B" at index 1
  3. Remove "F" from index 3

What I've tried

I've tried using the ToObservableChangeSet extension, but it doesn't have the correct behavior for my case.

Code
Subject<IReadOnlyList<string>> source = new Subject<IReadOnlyList<string>>();
IObservable<IEnumerable<string>> observableOfList = source;
IObservable<IChangeSet<string>> observableOfChangeSet = source.ToObservableChangeSet<string>();

observableOfChangeSet
    .Bind(out ReadOnlyObservableCollection<string> boundCollection)
    .Subscribe();

((INotifyCollectionChanged)boundCollection).CollectionChanged += OnCollectionChanged;

source.OnNext(new[] { "A", "C", "F" });
source.OnNext(new[] { "A", "B", "C" });

void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    Debug.WriteLine($"[{string.Join(", ", (IEnumerable<string>)sender)}] (operation: {e.Action} at index {e.NewStartingIndex})");
}
Output
[A, C, F] (operation: Reset at index -1)
[A, C, F, A] (operation: Add at index 3)
[A, C, F, A, B] (operation: Add at index 4)
[A, C, F, A, B, C] (operation: Add at index 5)

As we can see [A, C, F, A, B, C] is not the same as [A, B, C].


I've also tried using EditDiff, but that doesn't preserve the order of the list.

Code
Subject<IReadOnlyList<string>> source = new Subject<IReadOnlyList<string>>();
IObservable<IEnumerable<string>> observableOfList = source;
IObservable<IChangeSet<string>> observableOfChangeSet = ObservableChangeSet.Create<string>(list =>
{
    return observableOfList
        .Subscribe(items => list.EditDiff(items, EqualityComparer<string>.Default));
});

observableOfChangeSet
    .Bind(out ReadOnlyObservableCollection<string> boundCollection)
    .Subscribe();

((INotifyCollectionChanged)boundCollection).CollectionChanged += OnCollectionChanged;

source.OnNext(new[] { "A", "C", "F" });
source.OnNext(new[] { "A", "B", "C" });

void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    Debug.WriteLine($"[{string.Join(", ", (IEnumerable<string>)sender)}] (operation: {e.Action} at index {e.NewStartingIndex})");
}
Output
[A, C, F] (operation: Reset at index -1)
[A, C] (operation: Remove at index -1)
[A, C, B] (operation: Add at index 2)

As we can see [A, C, B] is not the same as [A, B, C].

Thanks!


Solution

  • I ended up writing a custom solution.

    It's open source and nuget packages are also available.

    To solve the original issue, install the following packages.

    • CollectionTracking.DynamicData
    • System.Reactive

    Then you can use a method like the following.

    using System.Reactive.Linq;
    using CollectionTracking;
    // ...
    public static class DynamicDataExtensions
    {
        /// <summary>
        /// Converts <see cref="IObservable{IEnumerable}"/> to <see cref="IObservableList{T}"/>.
        /// Keeps the same ordering, which is why we use .GetOperations and not .EditDiff.
        /// </summary>
        /// <typeparam name="T">Type of items in list.</typeparam>
        /// <param name="observableOfList"><see cref="IObservable{IEnumerable}"/> to convert.</param>
        /// <returns>Converted <see cref="IObservableList{T}"/>.</returns>
        public static IObservableList<T> ToObservableList<T>(this IObservable<IEnumerable<T>> observableOfList)
        {
            return observableOfList
                .StartWith(Enumerable.Empty<T>())
                .Buffer(2, 1)
                .Select(lists => lists[0].GetOperations(lists[1]).ToChangeSet())
                .AsObservableList();
        }
    }