-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Fixes BindableLayout BindingContext inheritance #17290
Fixes BindableLayout BindingContext inheritance #17290
Conversation
Hey there @albyrock87! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not following how clearing the BindingContext
on children solves a memory leak. Usually leaks are MAUI views that live forever -- not model objects. If a model object lives forever, it means there is a MAUI view that lives forever -- that is the actual problem to be solved.
Looking at:
We should be able to write a test with BindableLayout
as mentioned here:
https://github.com/dotnet/maui/wiki/Memory-Leaks#writing-tests
If we can show an issue with GC.Collect()
and WeakReference
that will prove what is going wrong here.
@jonathanpeppers I will add more unit tests to proof what is described in the issue, but besides that, the problem here is also a functional one. As I've explained, the binding context needs to be cleared the same way it happens with the automatic inheritance. The unit test I've added show exactly why the current implemention is wrong: because the removed view is still attached to the binding context. |
Hi @albyrock87. We have added the "s/pr-needs-author-input" label to this issue, which indicates that we have an open question/action for you before we can take further action. This PRwill be closed automatically in 14 days if we do not hear back from you by then - please feel free to re-open it if you come back to this PR after that time. |
4042d31
to
3c3042b
Compare
@jonathanpeppers some updates for you:
All the three unit tests are failing on Edit: the amend-commit below are just to rename things |
1d01752
to
1761325
Compare
var triggeredCount = 0; | ||
var itemTemplate = new DataTemplate(() => new MyViewModelBoundComponent(onTextChangedCallback: () => triggeredCount++)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove triggeredCount
and onTextChangedCallback
in these new tests? Nothing asserts against that value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The presence of that variable is what causes the leak, but this doesn't mean that the problem resides in the unit tests.
I guess it all depends on what the MAUI team says.
// The component should be gone | ||
Assert.Equal(0, MyViewModelBoundComponent.InstanceCount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we instead create a List<WeakReference>
that contains each MyViewModelBoundComponent
object, and assert IsAlive
is false
? Then you wouldn't need a manual InstanceCount
.
Example:
Assert.False(viewReference.IsAlive, $"{type} should not be alive!"); |
if (_myViewModel != null) | ||
{ | ||
_myViewModel.PropertyChanged += OnBindingContextFixturePropertyChanged; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this line the actual cause of the leak? You've created a control and viewmodel where the viewmodel has a strong reference to the control. Then at the top of the test you have a viewmodel that lives the lifetime of the test:
var myViewModel = new MyViewModel();
You would need to enclose myViewModel
in a scope, so the GC can collect it?
I'm still not sure there is an actual memory leak here, besides the one created in this test. Clearing the BindingContext
just makes the event unsubscribe in this custom control and remove the strong reference. I would consider not designing a custom control this way if the viewmodel is known to live longer than the control.
Maybe someone on the MAUI team can comment what the behavior is supposed to be in regards to BindingContext
. Should it always get cleared when children are unparented? @PureWeen @mattleibow @jsuarezruiz?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the point here is:
What should a View do if it needs to subscribe on a BindingContext property to do some action when something changes there?
How can it unsubscribe properly in the exact moment the view is no more needed?
The view cannot rely on the GC because that might happen at later moment in time, and something can happen in the mean time which would trigger an action on a removed view.
There are things out there on commercial libraries which rely on this behavior of clearing the context, and they work properly only when used outside of BindableLayout.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be correct to clear the BindingContext
(MAUI team can comment), but these tests have simply created a control that leaks unless its BindingContext
is cleared. It might be better to use WeakEventManager
or other solutions to avoid the problem entirely.
Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might make sense. Is the WeakEventManager instantly reacting to the un-reference of the child view?
Anyway for example, ObservableObject
in community toolkit does not use weak event manager.
I'm just saying this kind of situations happen in complex applications, and we need a clear way to handle them.
I felt that relaying on the way binding context inheritance works was the way.
1761325
to
5799552
Compare
@jonathanpeppers I see no response from the team, so I've investigated the code myself and it appears to me that clearing the context when the parent is unset is an explicit behavior in I've updated the title, description and commit to reflect that this PR is only fixing a wrong behavior. |
5799552
to
48a443f
Compare
- EmptyViewTemplate should inherit BindingContext even when it changes - BindingContext must be cleared on removed children to: - Behave like standard BindingContext inheritance - Avoid unwanted side-effects - Avoid leaking views in memory when they listen on BindingContext events
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't you use SetInheritedBindingContext instead of setting the child BindingContext ?
@StephaneDelcroix that'd be a nice idea, but it would cause
And the problem is that I'd like to do 3 right after 1, but |
Any updates on this? |
@PureWeen I am wondering if this should be using the visual childrent feature you added? I see you opened the OG issue, so not sure if you had thoughts? |
@PureWeen any chance to include this in the next 8 SR? |
/rebase |
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behavior now mirrors any other control we have that utilizes an ItemSource. CV and LV both also use this pattern when items are added/removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@StephaneDelcroix that'd be a nice idea, but it would cause
BindingContext
changing two times:
var view = (View)dataTemplate.CreateContent()
=> contextnull
layout.Add(view)
=> context =inherited from parent
BindableObject.SetInheritedBindingContext(view, item);
=> context =item
And the problem is that
3
will happen after the element is already in the visual tree.I'd like to do 3 right after 1, but
layout.Add
would override the value I've just set.
Chatted with @StephaneDelcroix some about this. We're going to see about adding an API that will achieve this goal. I did some local tests and I really like how this all works if we can set the item
as the Inherited Binding Context.
That basically lets us remove all the code that clears out the BC as well because removing the parent will just clear it out.
I feel like this is also an API we can extend out to CV/LV/etc..
@PureWeen should I decline the PR then and simply wait for the new API? |
Description of Change
Usually the
BindingContext
is automatically inherited viaElement.SetParent
usingSetChildInheritedBindingContext
.When an element is removed from a
Layout
, theElement.SetParent
takes care of clearingBindingContext
automatically.We can see that setting the
BindingContext
tonull
is an explicit behavior when the parent (value
) is set tonull
(a.k.a. child removed).BindableLayout
sets theBindingContext
manually on each created child, so it is its responsibility to clear theBindingContext
once the item is removed.Not clearing the
BindingContext
might cause unwanted side-effects and leaks if the child view attached to some of its events (i.e.PropertyChanged
).On top of that, the view created by
EmptyViewTemplate
should not have theBindingContext
set manually, because it matches the parent one: we should rely on the automatic inheritance.Issues Fixed
Issue #10904 does not represent a leak itself, but in a more complex scenario where a child attaches to some events on the
BindingContext
it would create issues.Issue #19142