Skip to content

Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are shared - #36746

Merged
kubaflo merged 6 commits into
dotnet:inflight/currentfrom
devanathan-vaithiyanathan:fix-39999
Jul 26, 2026
Merged

kubaflo merged 6 commits into
dotnet:inflight/currentfrom
devanathan-vaithiyanathan:fix-39999

Conversation

@devanathan-vaithiyanathan

Copy link
Copy Markdown
Contributor

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Issue Details

GradientBrush subscribes to each GradientStop's PropertyChanged event, but GradientStops.Clear() raises a collection Reset with e.OldItems == null. Because GradientBrush.OnGradientStopCollectionChanged only unsubscribes stops from e.OldItems, cleared stops remain subscribed.

Description of Change

Added a dictionary to track GradientStop subscriptions and manage event subscriptions correctly. Updated the collection change handling to support all collection actions, including Add, Remove, Replace, Move, and Reset. Fixed the Reset scenario by properly unsubscribing from existing GradientStop instances before re-subscribing to the current collection items. Also added reference counting to correctly handle duplicate GradientStop instances and refactored the collection attach/detach logic into helper methods to improve code maintainability.

Issues Fixed

Fixes #36743

Tested the behavior in the following platforms.

  • Android
  • Windows
  • iOS
  • Mac
Before After
Android
Before.mov
Android
After.mov

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36746

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36746"

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Jul 23, 2026
@devanathan-vaithiyanathan devanathan-vaithiyanathan added the community ✨ Community Contribution label Jul 23, 2026
@Tamilarasan-Paranthaman Tamilarasan-Paranthaman added platform/macos macOS / Mac Catalyst platform/windows platform/android area-drawing Shapes, Borders, Shadows, Graphics, BoxView, custom drawing platform/ios labels Jul 23, 2026
@devanathan-vaithiyanathan devanathan-vaithiyanathan changed the title [WIP] Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are shared Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are shared Jul 23, 2026
@sheiksyedm
sheiksyedm marked this pull request as ready for review July 23, 2026 15:11
Copilot AI review requested due to automatic review settings July 23, 2026 15:11
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a memory leak in GradientBrush where GradientStops.Clear() (Reset notifications with OldItems == null) could leave GradientStop.PropertyChanged handlers subscribed, keeping brushes alive when stops are shared.

Changes:

  • Refactors GradientBrush’s collection attach/detach logic and expands CollectionChanged handling to cover all actions, including Reset.
  • Introduces subscription tracking with reference counting to correctly subscribe/unsubscribe stops (including duplicate instances in the collection).
  • Adds a unit test ensuring Clear() unsubscribes previous gradient stops and prevents spurious invalidations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Controls/src/Core/GradientBrush.cs Adds ref-counted subscription tracking and robust NotifyCollectionChangedAction handling, including Reset resubscription.
src/Controls/tests/Core.UnitTests/LinearGradientBrushTests.cs Adds a regression test validating Clear() unsubscribes old stops from PropertyChanged.

Comment thread src/Controls/src/Core/GradientBrush.cs
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jul 23, 2026
@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Jul 23, 2026
MauiBot

This comment was marked as outdated.

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert Review — 2 findings

See inline comments for details.

Comment thread src/Controls/src/Core/GradientBrush.cs Outdated
Comment thread src/Controls/tests/Core.UnitTests/LinearGradientBrushTests.cs
kubaflo pushed a commit that referenced this pull request Aug 12, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Aug 12, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
@sheiksyedm sheiksyedm modified the milestones: .NET 10 SR10, .NET 10 SR11 Aug 26, 2026
kubaflo pushed a commit that referenced this pull request Aug 27, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Aug 27, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 2, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 2, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 3, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 3, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 3, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 3, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 4, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 4, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 6, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 6, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 7, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 7, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 7, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 7, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 8, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 8, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 8, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 8, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 9, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 9, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
kubaflo pushed a commit that referenced this pull request Sep 9, 2026
…shared (#36746)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
GradientBrush subscribes to each GradientStop's PropertyChanged event,
but GradientStops.Clear() raises a collection Reset with e.OldItems ==
null. Because GradientBrush.OnGradientStopCollectionChanged only
unsubscribes stops from e.OldItems, cleared stops remain subscribed.

### Description of Change

<!-- Enter description of the fix in this section -->
Added a dictionary to track GradientStop subscriptions and manage event
subscriptions correctly. Updated the collection change handling to
support all collection actions, including Add, Remove, Replace, Move,
and Reset. Fixed the Reset scenario by properly unsubscribing from
existing GradientStop instances before re-subscribing to the current
collection items. Also added reference counting to correctly handle
duplicate GradientStop instances and refactored the collection
attach/detach logic into helper methods to improve code maintainability.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #36743

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **Android**<br> <video
src="https://github.com/user-attachments/assets/6c924225-c68a-44e6-8062-e705eaeb8402"
width="600" height="300"> | **Android**<br> <video
src="https://github.com/user-attachments/assets/28315b3b-05a6-4935-8f86-0316058986a4"
width="600" height="300"> |
kubaflo added a commit that referenced this pull request Sep 9, 2026
…GradientBrush (#36808)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Problem
`GradientBrush` (added in #36746) keys its subscription ref-count
dictionary with
`System.Collections.Generic.ReferenceEqualityComparer.Instance`, which
is **.NET 5+ only**. `Controls.Core` also targets
**netstandard2.0/2.1**, so the shared build breaks:

```
src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: 'ReferenceEqualityComparer' is inaccessible due to its protection level [Controls.Core.csproj::TargetFramework=netstandard2.0]
```

This blocks every build of `inflight/current` (including Copilot PR
reviews on inflight PRs, e.g. #36776, where the gate could not build the
base).

### Fix
Replace the BCL comparer with a small self-contained reference-identity
comparer that works on **every** TFM — equality by object reference,
hash from `RuntimeHelpers.GetHashCode` — preserving the leak fix's exact
semantics.

The other `ReferenceEqualityComparer` uses (`ShellRenderer.cs`,
`MapHandler.Android.cs`) are in platform-specific files that only
compile for .NET TFMs, so they are unaffected.

### Risk
Minimal — behaviorally identical to
`ReferenceEqualityComparer.Instance`; no public API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 25, 2026
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 community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android platform/ios platform/macos macOS / Mac Catalyst platform/windows s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GradientBrush.GradientStops.Clear() leaks brushes when stops are shared

7 participants