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. |
jonathanpeppers
left a comment
There was a problem hiding this comment.
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.
Can we remove triggeredCount and onTextChangedCallback in these new tests? Nothing asserts against that value.
There was a problem hiding this comment.
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.
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:
| if (_myViewModel != null) | ||
| { | ||
| _myViewModel.PropertyChanged += OnBindingContextFixturePropertyChanged; | ||
| } |
There was a problem hiding this comment.
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.
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.
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.
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
StephaneDelcroix
left a comment
There was a problem hiding this comment.
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). |
PureWeen
left a comment
There was a problem hiding this comment.
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
PureWeen
left a comment
There was a problem hiding this comment.
@StephaneDelcroix that'd be a nice idea, but it would cause
BindingContextchanging two times:
var view = (View)dataTemplate.CreateContent()=> contextnulllayout.Add(view)=> context =inherited from parentBindableObject.SetInheritedBindingContext(view, item);=> context =itemAnd the problem is that
3will happen after the element is already in the visual tree.I'd like to do 3 right after 1, but
layout.Addwould 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
BindingContextis automatically inherited viaElement.SetParentusingSetChildInheritedBindingContext.When an element is removed from a
Layout, theElement.SetParenttakes care of clearingBindingContextautomatically.We can see that setting the
BindingContexttonullis an explicit behavior when the parent (value) is set tonull(a.k.a. child removed).BindableLayoutsets theBindingContextmanually on each created child, so it is its responsibility to clear theBindingContextonce the item is removed.Not clearing the
BindingContextmight 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
EmptyViewTemplateshould not have theBindingContextset 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
BindingContextit would create issues.Issue #19142