Skip to content
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

VisualElement Background/Shadow Notify code creates a strong relationship between the brush and the element #12344

Closed
PureWeen opened this issue Dec 29, 2022 · 1 comment · Fixed by #13656
Assignees
Labels
area-drawing Shapes, Borders, Shadows, Graphics, BoxView, custom drawing fixed-in-8.0.0-preview.3.8149 Look for this fix in 8.0.0-preview.3.8149!

Comments

@PureWeen
Copy link
Member

PureWeen commented Dec 29, 2022

Description

Overall, we need to create a generalized strategy for handling Brush changes. There are 5 or 6 places where we use brushes but currently Background and Shape are the only ones that have some notification code.

Currently this is the code we have for Background but it's problematic.

void NotifyBackgroundChanges()
{
if (Background != null)
{
Background.Parent = this;
Background.PropertyChanged += OnBackgroundChanged;
if (Background is GradientBrush gradientBrush)
gradientBrush.InvalidateGradientBrushRequested += InvalidateGradientBrushRequested;
}
}
void StopNotifyingBackgroundChanges()
{
if (Background != null)
{
Background.Parent = null;
Background.PropertyChanged -= OnBackgroundChanged;
if (Background is GradientBrush gradientBrush)
gradientBrush.InvalidateGradientBrushRequested -= InvalidateGradientBrushRequested;
}
}

  • The first low hanging fruit issue here is that the Parent property probably shouldn't get set for every single type of brush. For example, right now if you have the following code
new TabbedPage()
{
     Title = "Tabbed Page",
     Background = SolidColorBrush.Green
};

That brush is an ImmutableBrush that will be reused everywhere that SolidColorBrush.Green is being used.

This can cause undesirable side effects because an element that's no longer on the screen might be set as the Parent of SolidColorBrush. This can cause events to propagate up from the brush to the element that's no longer on the screen. I saw this when running the iOS device tests. Whenever I ran a test for a second time it was causing a threading exception because the previous TabbedPage was set as parent on the ImmutableBrush which would then propagate up to the previous test run.

We can easily cover ImmutableBrush cases.

  • The second part here that I'm not sure about is if VisualElement should subscribe to the PropertyChanged event and if Parent should be set on any of the Brushes. Since brushes can be reused (StaticResource) this seems like it will cause undesirable behavior. I think we need a different weaker pattern here for propagating brush changes up.
@PureWeen PureWeen changed the title VisualElement Background Notify code creates a strong relationship between the brush and the element VisualElement Background/Shadow Notify code creates a strong relationship between the brush and the element Dec 29, 2022
@PureWeen PureWeen added the area-drawing Shapes, Borders, Shadows, Graphics, BoxView, custom drawing label Dec 29, 2022
@rachelkang rachelkang added this to the Backlog milestone Jan 5, 2023
@ghost
Copy link

ghost commented Jan 5, 2023

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@jsuarezruiz jsuarezruiz self-assigned this Jan 8, 2023
jonathanpeppers added a commit to jonathanpeppers/maui that referenced this issue Mar 2, 2023
Fixes: dotnet#12344
Fixes: dotnet#13557
Context: https://github.com/dotnet-presentations/dotnet-maui-workshop

While testing the `Monkey Finder` sample, I found the following
scenario causes an issue:

1. Declare a `{StaticResource}` `Brush` at the `Application` level,
   with a lifetime of the entire application.

2. Set `Background` on an item in a `CollectionView`, `ListView`, etc.

3. Scroll a lot, navigate away, etc.

4. The `Brush` will hold onto any `View`'s indefinitely.

The core problem here being `VisualElement` does:

    void NotifyBackgroundChanges()
    {
        if (Background is ImmutableBrush)
            return;

        if (Background != null)
        {
            Background.Parent = this;
            Background.PropertyChanged += OnBackgroundChanged;

            if (Background is GradientBrush gradientBrush)
                gradientBrush.InvalidateGradientBrushRequested += InvalidateGradientBrushRequested;
        }
    }

If no user code sets `Background` to `null`, these events remain
subscribed.

To fix this:

1. Create a `WeakNotifyCollectionChangedProxy` type for event subscription.

2. Don't set `Background.Parent = this`

~~ General Cleanup ~~

Through doing other fixes related to memory leaks & C# events, we've
started to gain a collection of `WeakEventProxy`-related types.

I created some core `internal` types to be reused:

* `abstract WeakEventProxy<TSource, TEventHandler>`
* `WeakNotifyCollectionChangedProxy`

The following classes now make use of these new shared types:

* `BindingExpression`
* `BindableLayout`
* `ListProxy`
* `VisualElement`

This should hopefully reduce mistakes and reuse code in this area.

~~ Concerns ~~

Since, we are no longer doing:

    Background.Parent = this;

This is certainly a behavior change. It is now replaced with:

    SetInheritedBindingContext(Background, BindingContext);

I had to update one unit test that was asserting `Background.Parent`,
which can assert `Background.BindingContext` instead.

As such, this change is definitely sketchy and I wouldn't backport to
.NET 7 immediately. We might test it out in a .NET 8 preview first.
jonathanpeppers added a commit to jonathanpeppers/maui that referenced this issue Mar 2, 2023
Fixes: dotnet#12344
Fixes: dotnet#13557
Context: https://github.com/dotnet-presentations/dotnet-maui-workshop

While testing the `Monkey Finder` sample, I found the following
scenario causes an issue:

1. Declare a `{StaticResource}` `Brush` at the `Application` level,
   with a lifetime of the entire application.

2. Set `Background` on an item in a `CollectionView`, `ListView`, etc.

3. Scroll a lot, navigate away, etc.

4. The `Brush` will hold onto any `View`'s indefinitely.

The core problem here being `VisualElement` does:

    void NotifyBackgroundChanges()
    {
        if (Background is ImmutableBrush)
            return;

        if (Background != null)
        {
            Background.Parent = this;
            Background.PropertyChanged += OnBackgroundChanged;

            if (Background is GradientBrush gradientBrush)
                gradientBrush.InvalidateGradientBrushRequested += InvalidateGradientBrushRequested;
        }
    }

If no user code sets `Background` to `null`, these events remain
subscribed.

To fix this:

1. Create a `WeakNotifyCollectionChangedProxy` type for event subscription.

2. Don't set `Background.Parent = this`

~~ General Cleanup ~~

Through doing other fixes related to memory leaks & C# events, we've
started to gain a collection of `WeakEventProxy`-related types.

I created some core `internal` types to be reused:

* `abstract WeakEventProxy<TSource, TEventHandler>`
* `WeakNotifyCollectionChangedProxy`

The following classes now make use of these new shared types:

* `BindingExpression`
* `BindableLayout`
* `ListProxy`
* `VisualElement`

This should hopefully reduce mistakes and reuse code in this area.

~~ Concerns ~~

Since, we are no longer doing:

    Background.Parent = this;

This is certainly a behavior change. It is now replaced with:

    SetInheritedBindingContext(Background, BindingContext);

I had to update one unit test that was asserting `Background.Parent`,
which can assert `Background.BindingContext` instead.

As such, this change is definitely sketchy and I wouldn't backport to
.NET 7 immediately. We might test it out in a .NET 8 preview first.
mattleibow pushed a commit that referenced this issue Mar 8, 2023
* [controls] fix memory leak in `VisualElement.Background`

Fixes: #12344
Fixes: #13557
Context: https://github.com/dotnet-presentations/dotnet-maui-workshop

While testing the `Monkey Finder` sample, I found the following
scenario causes an issue:

1. Declare a `{StaticResource}` `Brush` at the `Application` level,
   with a lifetime of the entire application.

2. Set `Background` on an item in a `CollectionView`, `ListView`, etc.

3. Scroll a lot, navigate away, etc.

4. The `Brush` will hold onto any `View`'s indefinitely.

The core problem here being `VisualElement` does:

    void NotifyBackgroundChanges()
    {
        if (Background is ImmutableBrush)
            return;

        if (Background != null)
        {
            Background.Parent = this;
            Background.PropertyChanged += OnBackgroundChanged;

            if (Background is GradientBrush gradientBrush)
                gradientBrush.InvalidateGradientBrushRequested += InvalidateGradientBrushRequested;
        }
    }

If no user code sets `Background` to `null`, these events remain
subscribed.

To fix this:

1. Create a `WeakNotifyCollectionChangedProxy` type for event subscription.

2. Don't set `Background.Parent = this`

~~ General Cleanup ~~

Through doing other fixes related to memory leaks & C# events, we've
started to gain a collection of `WeakEventProxy`-related types.

I created some core `internal` types to be reused:

* `abstract WeakEventProxy<TSource, TEventHandler>`
* `WeakNotifyCollectionChangedProxy`

The following classes now make use of these new shared types:

* `BindingExpression`
* `BindableLayout`
* `ListProxy`
* `VisualElement`

This should hopefully reduce mistakes and reuse code in this area.

~~ Concerns ~~

Since, we are no longer doing:

    Background.Parent = this;

This is certainly a behavior change. It is now replaced with:

    SetInheritedBindingContext(Background, BindingContext);

I had to update one unit test that was asserting `Background.Parent`,
which can assert `Background.BindingContext` instead.

As such, this change is definitely sketchy and I wouldn't backport to
.NET 7 immediately. We might test it out in a .NET 8 preview first.

* Auto-format source code

* Add test & subscribe to InvalidateGradientBrushRequested

---------

Co-authored-by: GitHub Actions Autoformatter <autoformat@example.com>
@ghost ghost locked as resolved and limited conversation to collaborators Apr 7, 2023
@samhouts samhouts modified the milestones: Backlog, .NET 8 May 24, 2023
@samhouts samhouts added the fixed-in-8.0.0-preview.3.8149 Look for this fix in 8.0.0-preview.3.8149! label Jun 8, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-drawing Shapes, Borders, Shadows, Graphics, BoxView, custom drawing fixed-in-8.0.0-preview.3.8149 Look for this fix in 8.0.0-preview.3.8149!
Projects
None yet
4 participants