diff --git a/Realm/Realm/DatabaseTypes/RealmValue.cs b/Realm/Realm/DatabaseTypes/RealmValue.cs index 38a522bfd1..04b7740e1f 100644 --- a/Realm/Realm/DatabaseTypes/RealmValue.cs +++ b/Realm/Realm/DatabaseTypes/RealmValue.cs @@ -789,13 +789,17 @@ public T AsRealmObject() where T : class, IRealmObjectBase => Type == RealmValueType.Null ? null : AsRealmObject(); + // TODO (ni): add docs public T AsMappedObject() where T : class, IMappedObject { EnsureType("dictionary", RealmValueType.Dictionary); - throw new NotImplementedException(); + var result = Activator.CreateInstance(); + result.SetBackingStorage(_dictionaryValue!); + return result; } + // TODO (ni): add docs public T? AsNullableMappedObject() where T : class, IMappedObject => Type == RealmValueType.Null ? null : AsMappedObject(); @@ -815,12 +819,17 @@ public T As() if (typeof(IMappedObject).IsAssignableFrom(typeof(T))) { - return Type switch + switch (Type) { - RealmValueType.Null => Operator.Convert(null)!, - RealmValueType.Dictionary => throw new NotImplementedException(), - _ => throw new NotSupportedException($"Can't convert from {Type} to dictionary, which is the backing storage type for {typeof(T)}"), - }; + case RealmValueType.Null: + return Operator.Convert(null)!; + case RealmValueType.Dictionary: + var result = Activator.CreateInstance()!; + ((IMappedObject)result).SetBackingStorage(_dictionaryValue!); + return result; + default: + throw new InvalidCastException($"Can't convert from {Type} to dictionary, which is the backing storage type for {typeof(T)}"); + } } // This largely copies AsAny to avoid boxing the underlying value in an object diff --git a/Realm/Realm/Extensions/CollectionExtensions.cs b/Realm/Realm/Extensions/CollectionExtensions.cs index 54a7f81eee..96d41df999 100644 --- a/Realm/Realm/Extensions/CollectionExtensions.cs +++ b/Realm/Realm/Extensions/CollectionExtensions.cs @@ -557,6 +557,15 @@ public static async Task> SubscribeAsync(this IQueryable que return query; } + // TODO (ni): add docs + public static T As(this IDictionary dict) + where T : IMappedObject + { + var result = Activator.CreateInstance(); + result.SetBackingStorage(dict); + return result; + } + [EditorBrowsable(EditorBrowsableState.Never)] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "This is only used by the weaver/source generated classes and should not be exposed to users.")] diff --git a/Tests/Realm.Tests/Database/FlexibleSchemaPocTests.cs b/Tests/Realm.Tests/Database/FlexibleSchemaPocTests.cs index 320c2736a6..32c3aba461 100644 --- a/Tests/Realm.Tests/Database/FlexibleSchemaPocTests.cs +++ b/Tests/Realm.Tests/Database/FlexibleSchemaPocTests.cs @@ -16,8 +16,11 @@ // //////////////////////////////////////////////////////////////////////////// +using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Runtime.CompilerServices; using NUnit.Framework; namespace Realms.Tests.Database; @@ -26,15 +29,45 @@ namespace Realms.Tests.Database; public partial class FlexibleSchemaPocTests : RealmInstanceTest { [Test] - public void ConvertDictionary_ToMappedType() + public void RealmValue_AsMappedType_ReturnsCorrectObject() { - // TODO: NI to get this to work AddData(); var dogContainer = _realm.All().First(c => c.ContainedObjectType == nameof(Dog)); var dog = dogContainer.MixedProperty.As(); - // var dogFromDict = dogContainer.MixedDict.As(); + + // TODO: add assertions for the values + Assert.That(dog, Is.TypeOf()); + + var dog2 = dogContainer.MixedProperty.AsMappedObject(); + + // TODO: add assertions for the values + Assert.That(dog2, Is.TypeOf()); + + var nullContainer = _realm.Write(() => _realm.Add(new FlexibleSchemaPocContainer("null") + { + MixedProperty = RealmValue.Null + })); + + var nullDog = nullContainer.MixedProperty.As(); + Assert.That(nullDog, Is.Null); + + var nullDog2 = nullContainer.MixedProperty.AsNullableMappedObject(); + Assert.That(nullDog2, Is.Null); + } + + [Test] + public void RealmValue_AsMappedType_WhenTypeIsIncorrect_Throws() + { + var intContainer = _realm.Write(() => _realm.Add(new FlexibleSchemaPocContainer("int") + { + MixedProperty = 5 + })); + + Assert.Throws(() => intContainer.MixedProperty.As()); + Assert.Throws(() => intContainer.MixedProperty.AsMappedObject()); + Assert.Throws(() => intContainer.MixedProperty.AsNullableMappedObject()); } [Test] @@ -59,6 +92,43 @@ public void AccessMappedTypeProperties_ReadsValuesFromBackingStorage() Assert.That(bird.CanFly, Is.True); } + [Test] + public void NotifyPropertyChanged_NotifiesForModifications() + { + AddData(); + + var dogContainer = _realm.All().First(c => c.ContainedObjectType == nameof(Dog)); + + var dog = dogContainer.MixedProperty.As(); + var changes = new List(); + dog.PropertyChanged += (s, e) => + { + Assert.That(s, Is.EqualTo(dog)); + changes.Add(e); + }; + + _realm.Write(() => + { + dogContainer.MixedProperty.AsDictionary()[nameof(Dog.BarkCount)] = 10; + }); + + _realm.Refresh(); + + Assert.That(changes.Count, Is.EqualTo(1)); + Assert.That(changes[0].PropertyName, Is.EqualTo(nameof(Dog.BarkCount))); + + _realm.Write(() => + { + dogContainer.MixedProperty.AsDictionary()[nameof(Dog.BarkCount)] = 15; + dogContainer.MixedProperty.AsDictionary()[nameof(Dog.Name)] = "Fido III"; + }); + _realm.Refresh(); + + Assert.That(changes.Count, Is.EqualTo(3)); + Assert.That(changes[1].PropertyName, Is.EqualTo(nameof(Dog.BarkCount))); + Assert.That(changes[2].PropertyName, Is.EqualTo(nameof(Dog.Name))); + } + private void AddData() { _realm.Write(() => @@ -115,7 +185,7 @@ public partial class Dog : IMappedObject } // Generated - public partial class Dog + public partial class Dog : INotifyPropertyChanged { private IDictionary _backingStorage = null!; @@ -123,6 +193,69 @@ public void SetBackingStorage(IDictionary dictionary) { _backingStorage = dictionary; } + + #region INotifyPropertyChanged + + private IDisposable? _notificationToken; + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + _notificationToken = _backingStorage.SubscribeForKeyNotifications((sender, changes) => + { + if (changes == null) + { + return; + } + + foreach (var key in changes.ModifiedKeys) + { + RaisePropertyChanged(key); + } + + // TODO: what do we do with deleted/inserted keys + }); + } + + private void UnsubscribeFromNotifications() + { + _notificationToken?.Dispose(); + } + + #endregion } // User-defined @@ -134,7 +267,7 @@ public partial class Bird : IMappedObject } // Generated - public partial class Bird + public partial class Bird : INotifyPropertyChanged { private IDictionary _backingStorage = null!; @@ -142,5 +275,68 @@ public void SetBackingStorage(IDictionary dictionary) { _backingStorage = dictionary; } + + #region INotifyPropertyChanged + + private IDisposable? _notificationToken; + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + _notificationToken = _backingStorage.SubscribeForKeyNotifications((sender, changes) => + { + if (changes == null) + { + return; + } + + foreach (var key in changes.ModifiedKeys) + { + RaisePropertyChanged(key); + } + + // TODO: what do we do with deleted/inserted keys + }); + } + + private void UnsubscribeFromNotifications() + { + _notificationToken?.Dispose(); + } + + #endregion } }