Ugh... Yeah, that's not really surprising, but I don't have a good idea how to resolve it. The reason two-way databinding works with Realm is because we do some reflection black magic. Classic bindings in Xamarin Forms use reflection to try and invoke the setters/getters of the properties they are bound to. But before they try to go for the full-blown reflection, they'll inspect the object and see if it implements IReflectableType
. RealmObject inheritors do implement it and we give the binding engine a fake setter that will open a transaction, invoke the original setter, then commit the transaction (https://github.com/realm/realm-dotnet/blob/master/Realm/Realm/DataBinding/WovenSetterMethodInfo.cs#L60-L78). We've chosen this approach because:
1) We want transactions to be explicit when invoking the setters from code. Committing a transaction introduces significant overhead when setting a property, so we want to encourage people to think about that and try to batch multiple object updates in a single transaction. So invoking the regular property setters should always be done within a transaction.
2) We understand that a developer doesn't have a lot of control about what the binding engine does and we want to provide a somewhat seamless experience, at the expense of reduced performance for updates made from the binding engine. We're not too worried about the performance there because these updates are typically triggered by a user action and it's unlikely that a user is able to generate more than a few tens of actions per second which is slow enough for Realm to commit transactions without introducing noticeable delay on the main thread (a transaction commit takes between 1 and 5 ms on average). This means that the developer doesn't have to worry about opening a transaction for the UI changes.
So far this approach has worked great - we had a clear distinction between calls from the UI (those would use IReflectableType
) and calls from code (which use the regular setters). With compiled bindings though, you're explicitly telling the binding engine what the type is, which means that it now has enough information to invoke the setter directly, meaning it circumvents our IReflectableType
magic and makes it impossible for us to identify the setter invocation as coming from the UI.
Now, after all of that context, there are a few mitigations I can think of, none of which is great:
1. Don't use compiled bindings 😅 Now, I know this may not sound ideal, but when binding to Realm objects, the runtime binding overhead is much smaller than in other cases. This is because Realm's implementation has both type and property caches, which means that after the first invocation for each type, there'll be insignificant overhead from using classic bindings. If you want to speed up bindings at the expense of a slightly longer startup time, you can manually iterate over all object types and force creation of the type info via IReflectableType
, which will generate the cache, after which bindings should be nearly as fast as compiled ones. As with all things performance, it's best to measure to verify, but this is by far the easiest option.
2. Add your own property getters/setters to the models that mimic the IReflectableType
fake getters/setters. So something like:
csharp
public class Person : RealmObject
{
public string Name { get; set; }
public string NameUI
{
get
{
if (this.IsValid)
{
return this.Name;
}
return null;
}
set
{
Transaction writeTransaction = null;
if (this.Realm != null && !this.Realm.IsInTransaction)
{
writeTransaction = this.Realm.BeginWrite();
}
this.Name = value;
if (writeTransaction != null)
{
writeTransaction.Commit();
writeTransaction.Dispose();
}
}
}
}
You can then use Name
when changing it in code and NameUI
for databinding. Obviously this is not great as it introduces a ton of boilerplate, but if the performance gain from using compiled bindings is significant, it might be worth sacrificing maintainability for performance.
3. This is a build-up on 2, but instead of you writing the boilerplate, we can introduce some convention where if you define a Name
and NameUI
properties in your model with automatic getter/setter, we'll generate the boilerplate for NameUI
to call Name
in a transaction. This is just throwing it out there and I don't love it, but we may consider doing something like that to support compiled bindings, although I expect it will take quite a bit of time before we align on an approach there.