Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
PawKanarek opened this issue Mar 15, 2022 · 5 comments · Fixed by #3254
Closed

Comments

@PawKanarek
Copy link

PawKanarek commented Mar 15, 2022

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

@PawKanarek PawKanarek changed the title [Bug]: NotifyCollectionChangedAction.Replace and is not fired [Bug]: NotifyCollectionChangedAction.Replace and Reset is not fired on RealmCollection Mar 15, 2022
@nirinchev
Copy link
Member

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
Copy link
Author

PawKanarek commented Mar 16, 2022

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
Copy link
Contributor

papafe commented Mar 16, 2022

@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:

  • Realm.Refresh() updates the realm to the latest version and also forces the sending of all outstanding notifications. This is something that happens organically when using realm on the main thread of an application, without the need to call Refresh() manually. In your case what was happening is that before you added the last Refresh call, the notification for Clear() was not being sent out yet. The penultimate call to Refresh was actually computing the notification, but because of the bug I've said before nothing was raised.
  • As @nirinchev said, we don't guarantee identical behaviour to ObservableCollection, but we try to respect the meaning of the NotifyCollectionChangedAction enum. Because Clear() just removes the content of the collection we raise Remove as it complies with the meaning of the enum. We try to resort to Reset only in extreme cases when we cannot raise any other event.

@nirinchev
Copy link
Member

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
Copy link
Author

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants