Как мы создаем IObservableList из IObservable<IList>?

#c# #.net #system.reactive #reactive #reactiveui

#c# #.net #system.reactive #реактивный

Вопрос:

Вопрос

Как мы переходим от IObservable<IReadOnlyList<T>> к IObservableList<T> (из DynamicData)?

Контекст

Я использую в своем проекте как реактивные расширения, так и DynamicData.

В настоящее время у меня есть список строк, который меняется со временем. У меня есть at как IObservable<IReadOnlyList<string>> . Он выдает обновления, подобные следующим:

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

Я хочу преобразовать это в IObservableList<string> , который будет выдавать обновления, подобные следующим:

  1. Добавьте «A», «C», «F» в индекс 0
  2. Добавьте «B» в индекс 1
  3. Удалить «F» из индекса 3

Что я пробовал

Я пытался использовать ToObservableChangeSet расширение, но оно не имеет правильного поведения для моего случая.

Код

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

Вывод

 [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)
  

Как мы можем видеть, [A, C, F, A, B, C] это не то же самое, что [A, B, C] .


Я также пытался использовать EditDiff , но это не сохраняет порядок списка.

Код

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

Вывод

 [A, C, F] (operation: Reset at index -1)
[A, C] (operation: Remove at index -1)
[A, C, B] (operation: Add at index 2)
  

Как мы можем видеть, [A, C, B] это не то же самое, что [A, B, C] .

Спасибо!

Комментарии:

1. Попробуйте вместо этого использовать EditDiff. Что — то вроде github.com/RolandPheasant/DynamicData.Snippets/blob /…

2. @GlennWatson Это почти сработало! Однако порядок списка не сохраняется с помощью EditDiff . Я отредактировал вопрос, чтобы добавить этот образец.

Ответ №1:

В итоге я написал пользовательское решение.

Это открытый исходный код, и пакеты nuget также доступны.

Чтобы решить исходную проблему, установите следующие пакеты.

  • CollectionTracking.DynamicData
  • System.Reactive

Затем вы можете использовать метод, подобный следующему.

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

Ответ №2:

Если строки уникальны, почему бы вам не использовать перегрузку ToObservableChangeSet , где вы устанавливаете строку в качестве ключа. Таким образом, если ваш новый список не содержит «F», он знает, что ваш существующий «F» должен быть удален.

Возможно, есть более простой способ, но вам, возможно, придется временно вставить свой собственный индекс. Если вам не нужно отслеживать изменения через связанную коллекцию, вы можете сделать это из набора изменений следующим образом:

 Subject<IReadOnlyList<string>> source = new Subject<IReadOnlyList<string>>();
IObservable<IEnumerable<string>> observableOfList = source;
IObservable<IChangeSet<(string,int),string>> observableOfChangeSet = source.Select(x => 
            {
                return x.Select((item, index) => (item, index));
            }).ToObservableChangeSet<(string,int),string>(x=>x.Item1);

observableOfChangeSet
    .Sort(SortExpressionComparer<(string, int)>.Ascending(x => x.Item2))
    .OnItemAdded(x=> Debug.WriteLine($"Item {x.Item1} was added to index {x.Item2}"))
    .OnItemRemoved(x => Debug.WriteLine($"Item {x.Item1} was removed from index {x.Item2}"))
    .OnItemUpdated((x,prev) => Debug.WriteLine($"Item {prev.Item1} at index {prev.Item2} was updated to Item {x.Item1} at index {x.Item2}"))
    .Transform(x=>
        {
            Debug.WriteLine($"Item: {x.Item1} at index: {x.Item2}");
           return x.Item1;
        })
    .Bind(out ReadOnlyObservableCollection<string> boundCollection)
    .Subscribe();

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

Мой отладочный вывод для этого кода:

 Item A was added to index 0
Item C was added to index 1
Item F was added to index 2
Item: A at index: 0
Item: C at index: 1
Item: F at index: 2
Item B was added to index 1
Item F was removed from index 2
Item A at index 0 was updated to Item A at index 0
Item C at index 1 was updated to Item C at index 2
Item: A at index: 0
Item: B at index: 1
Item: C at index: 2
  

Комментарии:

1. Умно! Я не уверен, что понимаю эту строку, хотя: Item A at index 0 was updated to Item A at index 0 ?

2. Я полагаю, что, поскольку новый список (A, B, C) содержит что-то, что уже существует в кэше (элемент с ключом «A»), предполагается, что я пытаюсь обновить его, даже если содержимое остается тем же. Это должно быть ограничением использования ToObservableChangeSet для преобразования наблюдаемых списков.

Ответ №3:

Я не слишком знаком с DynamicData, но я считаю, что это дает что-то близкое к желаемому результату:

 using DynamicData;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            Subject<IReadOnlyList<string>> source = new Subject<IReadOnlyList<string>>();

            var list = ObservableChangeSet.Create<string>(sourceList =>
            {
                return source
                    .Subscribe(newItems =>
                    {
                        sourceList.Edit(items =>
                        {
                            for (var i = 0; i < newItems.Count; i  )
                            {
                                if (items.Count <= i) items.Add(newItems[i]);
                                else
                                {
                                    if (items[i] != newItems[i])
                                    {
                                        items[i] = newItems[i];
                                    }
                                }
                            }
                            while (items.Count > items.Count) items.RemoveAt(items.Count - 1);
                        });
                    });
            }).AsObservableList();

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

            Console.ReadLine();
        }
    }
}
  

К сожалению, я не понял, как получить Add "B" at index 1 , но текущий результат выглядит следующим образом:

  1. AddRange. 3 changes
  2. 3 Changes
    • Replace. Current: B, Previous: C
    • Replace. Current: C, Previous: F
    • Add. Current: F, Previous: <None>
  3. Remove. Current: C, Previous: <None>

Редактировать: Ниже приведен точный результат, который вы хотите, но я не тестировал производительность каждого (вставка, вероятно, будет дорогостоящей для больших списков):

 using DynamicData;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            Subject<IReadOnlyList<string>> source = new Subject<IReadOnlyList<string>>();

            var list = ObservableChangeSet.Create<string>(sourceList =>
            {
                return source
                    .Subscribe(newItems =>
                    {
                        sourceList.Edit(items =>
                        {
                            for (var i = 0; i < newItems.Count; i  )
                            {
                                if (items.Count <= i) items.Add(newItems[i]);
                                else
                                {
                                    if (items[i] != newItems[i])
                                    {
                                        items.Insert(i, newItems[i]);
                                    }
                                }
                            }
                            while (items.Count > newItems.Count) items.RemoveAt(items.Count - 1);
                        });
                    });
            }).AsObservableList();

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

            Console.ReadLine();
        }
    }
}

  
  1. AddRange. 3 changes
  2. Add. Current: B, Previous: <None> Index 1
  3. Remove. Current: C, Previous: <None> Index 2

Комментарии:

1. @Batesias Я не уверен, есть ли уведомление для этого ответа, поскольку я удалил оригинал, поняв, что я неправильно понял вопрос.