realm / realm-dotnet

Realm is a mobile database: a replacement for SQLite & ORMs
https://realm.io
Apache License 2.0
1.26k stars 165 forks source link

[Bug]: NotifyCollectionChangedAction.Replace and Reset is not fired on RealmCollection #2854

Closed PawKanarek closed 1 year ago

PawKanarek commented 2 years ago

What happened?

Hi, I've noticed a bug where RealmCollection doesn't behave in the same way as ObservableCollection. From what I've observed, RealmCollection doesn't fire Replace & Reset from INotifyCollectionChanged Interface, thus my app is not updating the UI correctly when I switched from ObservableCollection to RealmDb in my ViewModels. I've made a very simple example to prove my point.

Repro steps

Launch provided code snippet in console application.

Version

net5.0 for console app & Xamarin for my main project issue

What SDK flavour are you using?

Local Database only

What type of application is this?

Xamarin

Client OS and version

Android, iOS, ConsoleApp in .net core Console app for macOS

Code snippets

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.Threading.Tasks;
using Nito.AsyncEx;
using Realms;

namespace ConsoleApp3
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            ObservableCollectionTest();
            AsyncContext.Run(RealmCollectionTest);
        }

        private static void ObservableCollectionTest()
        {
            var item0 = new RealmItem { Id = 0 };
            var item1 = new RealmItem { Id = 1 };
            var item2 = new RealmItem { Id = 2 };
            // add items to collection 
            var colllection = new MyCollection(new[] { item0, item1 });

            // listen for notification
            var actions = new List<NotifyCollectionChangedAction>(5);
            colllection.CollectionChanged += (sender, eventArgs) =>
            {
                actions.Add(eventArgs.Action);
            };

            colllection.Add(item2); // Add items
            colllection.MoveItem(1, 2); // Move items
            colllection.Remove(item0); // Remove items
            colllection[1] = item2; // Replace items
            colllection.Clear(); // Reset items

            Console.WriteLine($"Actions for ObservableCollection: {string.Join(", ", actions)}");
        }

        private static void RealmCollectionTest()
        {
            var path = Path.Combine(Environment.CurrentDirectory, "realm.realm");
            var realm = Realm.GetInstance(new RealmConfiguration(path));

            var item0 = new RealmItem { Id = 0 };
            var item1 = new RealmItem { Id = 1 };
            var item2 = new RealmItem { Id = 2 };
            var colllection = new RealmWithCollection { Id = "col" };
            // add items to persistent cache & to collection
            realm.Write(() =>
            {
                realm.RemoveAll();
                realm.Add(item0);
                realm.Add(item1);
                realm.Add(item2);
                realm.Add(colllection);
                colllection.Items.Add(item0);
                colllection.Items.Add(item1);
            });

            // listen for notification
            var actions = new List<NotifyCollectionChangedAction>(5);
            colllection.Items.AsRealmCollection().CollectionChanged += (sender, eventArgs) =>
            {
                actions.Add(eventArgs.Action);
            };

            realm.Write(() => colllection.Items.Add(item2)); // Add items
            realm.Write(() => colllection.Items.Move(item1, 2)); // Move items
            realm.Write(() => colllection.Items.Remove(item0)); // Remove items
            realm.Write(() => colllection.Items[1] = item2); // Replace items
            realm.Write(() => colllection.Items.Clear()); // Reset items

            Console.WriteLine($"Actions for RealmCollection: {string.Join(", ", actions)}");
        }

        class MyCollection : ObservableCollection<RealmItem>
        {
            public MyCollection(IEnumerable<RealmItem> collection) : base(collection) { }
            public new void MoveItem(int oldIndex, int newIndex)
            {
                base.MoveItem(oldIndex, newIndex);
            }
        }
        public class RealmWithCollection : RealmObject
        {
            [PrimaryKey] public string Id { get; set; }
            public IList<RealmItem> Items { get; }
        }
        public class RealmItem : RealmObject
        {
            [PrimaryKey] public int Id { get; set; }
        }
    }
}
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
      <PackageReference Include="Fody" Version="6.6.0">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      </PackageReference>
      <PackageReference Include="Nito.AsyncEx.Context" Version="5.1.2" />
      <PackageReference Include="Realm" Version="10.9.0" />
      <PackageReference Include="Realm.Fody" Version="10.9.0">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      </PackageReference>
    </ItemGroup>

</Project>

Relevant log output

Actions for ObservableCollection: Add, Move, Remove, Replace, Reset
Actions for RealmCollection: Add, Move, Remove

^ ObservableCollection & RealmCollection should fire INotifyCollectionChanged events in the same way

nirinchev commented 2 years ago

Thanks for the repro! While we don't guarantee 100% identical behavior with ObservableCollection, this does look like a bug and we'll investigate.

PawKanarek commented 2 years ago

Thanks. @papafe I've also noticed, that if i manually trigger realm.Refresh() after replacing items, then nothing happens, but when i trigger it after items.Clear() then i'm receiving new extra event Remove. Maybe this will help

realm.Write(() => colllection.Items.Add(item2)); // Add items
realm.Write(() => colllection.Items.Move(item1, 2)); // Move items
realm.Write(() => colllection.Items.Remove(item0)); // Remove items
realm.Write(() => colllection.Items[1] = item2); // Replace items (don't work)
realm.Refresh(); // NEW CODE COMPARING TO SNIPPET - does nothing 
realm.Write(() => colllection.Items.Clear()); // Reset items (after realm.Refresh(), generates new remove action)
realm.Refresh(); // NEW CODE COMPARING TO SNIPPET -  adds new "remove" action

Output:

Actions for ObservableCollection: Add, Move, Remove, Replace, Reset
Actions for RealmCollection: Add, Move, Remove, Remove  

^Thats extra Remove action comparing to previous snippet

For me right now the missing Replace action is biggest issue. Thank you for investigating :)

papafe commented 2 years ago

@PawKanarek I managed to reproduce the issue, thanks a lot for your detailed example! I just had to copy paste  😄

Regarding your main issue, the main problem here is that we do not raise the CollectionChanged event when the object gets replaced, and that's something we need to fix.

Regarding the behaviour you've noticed in the second message, that is expected. There are actually two things happening here:

nirinchev commented 2 years ago

We can probably special case the situation where the collection count is 0 and raise Reset as it's probably going to be faster for the UI to redraw the content.

PawKanarek commented 2 years ago

Thanks for investigating so fast :) Yes, i also think that Reset is not that important, because we have Remove events, so that's good. Also I agree that RealmCollecetion don't need to be exactly the same as ObservableCollection. I've created this bug mosty for Replace event, because my UI wasn't responding when items changed order in Realm Database.