CommunityToolkit / dotnet

.NET Community Toolkit is a collection of helpers and APIs that work for all .NET developers and are agnostic of any specific UI platform. The toolkit is maintained and published by Microsoft, and part of the .NET Foundation.
https://docs.microsoft.com/dotnet/communitytoolkit/?WT.mc_id=dotnet-0000-bramin
Other
2.98k stars 293 forks source link

[Feature] Add Helper Extension `IList<T>.SyncWith(IList<T> another, IEqualityComparer<T> comparer = null)` #774

Open jingkecn opened 11 months ago

jingkecn commented 11 months ago

Describe the problem

This might not be quite requiring, but it will indeed help a lot in some cases where we need to update the entire list ListViewBase.ItemSource without blinking the UI.

For example:

var data = await SomeDataService.FetchDataAsync() // IList<TDataType>;
var oc = new ObservableCollection<TDataType>(data);
MyListView.ItemSource = oc; // Update the entire list.

Each time we update the entire data list (with data binding or not), the UI will blink, this is not terrible but it would be better if we can have an auto diff tool to make it visually updated with only different elements.

Describe the solution

A helper extension IList<T>.SyncWith(IList<T> another, IEqualityComparer<T> comparer = null) could be an initial idea:

// Implementation
public static IList<TSource> SyncWith<TSource>(
    this IList<TSource> source,
    IList<TSource> another,
    IEqualityComparer<TSource> comparer = null)
{
  // Step 1: Remove redundant items, if any.
  source.Except(another).ToList().ForEach(x => _ = source.Remove(x));
  // Step 2: Update the list, index by index.
  for (var index = 0; index < another.Count; index++)
  {
    var newItem = another[index];
    if (index < source.Count)
    {
      var oldItem = source[index];
      if (comparer?.Equals(newItem, oldItem) ?? Equals(newItem, oldItem))
      {
        continue;
      }

      // Step 2-1: remove the old item, if it's been changed at the current position.
      source.RemoveAt(index);
    }

    // Step 2-2: insert the new item at the current position.
    source.Insert(index, newItem);
  }

  // Step 3: (last but not least) assertive verification.
  Debug.Assert(source.SequenceEqual(another, comparer));
  return source;
}

// Use Case
var data = await SomeDataService.FetchDataAsync(); // IList<TDataType>
var oc = new ObservableCollection<TDataType>(data);
MyListView.ItemSource = oc; // Initializes the entire list.

// When we need to update the entire list
data = await SomeDataService.FetchDataAsync(); // IList<TDataType>
_ = oc.SyncWith(data); // The UI won't blink and will apply the corresponding update animations.

Alternatives

No response

Additional info

No response

Help us help you

Yes, I'd like to be assigned to work on this item.

ghost commented 11 months ago

Hello, 'jingkecn! Thanks for submitting a new feature request. I've automatically added a vote 👍 reaction to help get things started. Other community members can vote to help us prioritize this feature in the future!

michael-hawker commented 11 months ago

@Sergio0694 this sounds similar to an example you did in the MVVM Toolkit Sample App, eh?

Moving this to the dotnet repo as a feature request as it deals with IList as the interface vs. anything to a specific framework.

jingkecn commented 11 months ago

@michael-hawker indeed, I did it with IList to make it as common as possible, thanks for moving it to the dotnet repo.

jingkecn commented 11 months ago

@michael-hawker Any updates? ☺️