Closed robertmiles3 closed 4 years ago
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:
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.IReflectableType
fake getters/setters. So something like:
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.
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.Hmm, gotcha. Thanks for the background info. That makes sense. I agree that none of the approaches are all that hot. However, the particular use-case that I'm needing is only binding 2 properties on the model, so approach 2 isn't horrible. Two questions....
[Ignored]
on it so as to not try and persist to the DB? Or is that already implied by having an explicit setter?this.Realm.BeginWrite();
should be writeTransaction = this.Realm.BeginWrite();
?Great. I just threw it in, and it works fine. No more crash. Thanks for the tip and so fast! You've been very helpful.
For anyone coming here later with the same issue, I added the following property to my model...
// Reasoning: https://github.com/realm/realm-dotnet/issues/1996
public string DefinitionUI
{
// https://github.com/realm/realm-dotnet/blob/master/Realm/Realm/DataBinding/WovenGetterMethodInfo.cs#L60-L74
get => this.IsValid ? this.Definition : null;
// https://github.com/realm/realm-dotnet/blob/master/Realm/Realm/DataBinding/WovenSetterMethodInfo.cs#L60-L78
set
{
var managingRealm = this?.Realm;
Transaction writeTransaction = null;
if (managingRealm != null && !managingRealm.IsInTransaction)
{
writeTransaction = managingRealm.BeginWrite();
}
this.Definition = value;
if (writeTransaction != null)
{
writeTransaction.Commit();
writeTransaction.Dispose();
}
}
}
...and then changed my binding from Definition
to DefinitionUI
and added back the x:DataType
for the compiled binding.
I recently attempted to enable compiled bindings in my Xamarin.Forms app, but I'm hitting a crash. If enable it on a read-only list, it works fine. However, if I enable it on a list where items in the list are editable (ie
Entry
) then I get aRealmInvalidTransactionException
. I want to use compiled bindings for better performance (see here).Goals
Enable compiled bindings on a
ListView
in Xamarin.Forms where the list items are editable Realm objects.Expected Results
List to function as normal
Actual Results
x:DataType="m:Item"
in this line (to enable compiled bindings), then we get the crash. If I removex:DataType
, all is well.Code Sample
https://github.com/robertmiles3/realmcrash2
Version of Realm and Tooling