Repository navigation
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are shared #36746
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
Changes from all commits
4e3a9b9
15ed1ec
2827497
bede25f
d6234c0
31e1444
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| #nullable disable | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Collections.Specialized; | ||
| using System.ComponentModel; | ||
|
|
||
|
|
@@ -9,6 +10,8 @@ | |
| [ContentProperty(nameof(GradientStops))] | ||
| public abstract class GradientBrush : Brush | ||
| { | ||
| readonly Dictionary<GradientStop, int> _subscriptionRefCounts = new(ReferenceEqualityComparer.Instance); | ||
|
Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs
|
||
|
|
||
| /// <summary>Initializes a new instance of the <see cref="GradientBrush"/> class.</summary> | ||
| public GradientBrush() | ||
| { | ||
|
|
@@ -42,64 +45,162 @@ | |
| base.OnBindingContextChanged(); | ||
|
|
||
| foreach (var gradientStop in GradientStops) | ||
| { | ||
| SetInheritedBindingContext(gradientStop, BindingContext); | ||
| } | ||
| } | ||
|
|
||
| void UpdateGradientStops(GradientStopCollection oldCollection, GradientStopCollection newCollection) | ||
| { | ||
| if (oldCollection != null) | ||
| DetachCollection(oldCollection); | ||
| AttachCollection(newCollection); | ||
| Invalidate(); | ||
| } | ||
|
|
||
| void AttachCollection(GradientStopCollection collection) | ||
| { | ||
| if (collection is null) | ||
| { | ||
| oldCollection.CollectionChanged -= OnGradientStopCollectionChanged; | ||
| return; | ||
| } | ||
|
|
||
| foreach (var oldStop in oldCollection) | ||
| { | ||
| oldStop.Parent = null; | ||
| oldStop.PropertyChanged -= OnGradientStopPropertyChanged; | ||
| } | ||
| collection.CollectionChanged += OnGradientStopCollectionChanged; | ||
|
|
||
| foreach (var stop in collection) | ||
| { | ||
| SubscribeToGradientStop(stop); | ||
| } | ||
| } | ||
|
|
||
| if (newCollection == null) | ||
| void DetachCollection(GradientStopCollection collection) | ||
| { | ||
| if (collection is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| newCollection.CollectionChanged += OnGradientStopCollectionChanged; | ||
| collection.CollectionChanged -= OnGradientStopCollectionChanged; | ||
| UnsubscribeFromAllGradientStops(); | ||
| } | ||
|
|
||
| foreach (var newStop in newCollection) | ||
| void OnGradientStopCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) | ||
| { | ||
| switch (e.Action) | ||
| { | ||
| if (newStop is not null) | ||
| { | ||
| newStop.Parent = this; | ||
| newStop.PropertyChanged += OnGradientStopPropertyChanged; | ||
| } | ||
| case NotifyCollectionChangedAction.Add: | ||
| if (e.NewItems is not null) | ||
| { | ||
| foreach (GradientStop stop in e.NewItems) | ||
| { | ||
| SubscribeToGradientStop(stop); | ||
| } | ||
| } | ||
| break; | ||
|
|
||
| case NotifyCollectionChangedAction.Remove: | ||
| if (e.OldItems is not null) | ||
| { | ||
| foreach (GradientStop stop in e.OldItems) | ||
| { | ||
| UnsubscribeFromGradientStop(stop); | ||
| } | ||
| } | ||
| break; | ||
|
|
||
| case NotifyCollectionChangedAction.Replace: | ||
| if (e.OldItems is not null) | ||
| { | ||
| foreach (GradientStop stop in e.OldItems) | ||
| { | ||
| UnsubscribeFromGradientStop(stop); | ||
| } | ||
| } | ||
| if (e.NewItems is not null) | ||
| { | ||
| foreach (GradientStop stop in e.NewItems) | ||
| { | ||
| SubscribeToGradientStop(stop); | ||
| } | ||
| } | ||
| break; | ||
|
|
||
| case NotifyCollectionChangedAction.Move: | ||
| // No subscription changes required. | ||
| break; | ||
|
|
||
| case NotifyCollectionChangedAction.Reset: | ||
| ResubscribeCollection(sender as GradientStopCollection); | ||
| break; | ||
| } | ||
|
|
||
| Invalidate(); | ||
| } | ||
|
|
||
| void OnGradientStopCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) | ||
| void SubscribeToGradientStop(GradientStop stop) | ||
| { | ||
| if (e.OldItems != null) | ||
| if (stop is null) | ||
| { | ||
| foreach (var oldItem in e.OldItems) | ||
| { | ||
| if (!(oldItem is GradientStop oldStop)) | ||
| continue; | ||
|
|
||
| oldStop.Parent = null; | ||
| oldStop.PropertyChanged -= OnGradientStopPropertyChanged; | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (e.NewItems != null) | ||
| if (_subscriptionRefCounts.TryGetValue(stop, out var count)) | ||
| { | ||
| foreach (var newItem in e.NewItems) | ||
| { | ||
| if (!(newItem is GradientStop newStop)) | ||
| continue; | ||
|
|
||
| newStop.Parent = this; | ||
| newStop.PropertyChanged += OnGradientStopPropertyChanged; | ||
| } | ||
| _subscriptionRefCounts[stop] = count + 1; | ||
| return; | ||
| } | ||
|
|
||
| Invalidate(); | ||
| _subscriptionRefCounts[stop] = 1; | ||
| stop.Parent = this; | ||
| stop.PropertyChanged += OnGradientStopPropertyChanged; | ||
| } | ||
|
|
||
| void UnsubscribeFromGradientStop(GradientStop stop) | ||
| { | ||
| if (stop is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (!_subscriptionRefCounts.TryGetValue(stop, out var count)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (count > 1) | ||
| { | ||
| _subscriptionRefCounts[stop] = count - 1; | ||
| return; | ||
| } | ||
|
|
||
| _subscriptionRefCounts.Remove(stop); | ||
| stop.Parent = null; | ||
| stop.PropertyChanged -= OnGradientStopPropertyChanged; | ||
|
Comment on lines
+175
to
+177
|
||
| } | ||
|
|
||
| void UnsubscribeFromAllGradientStops() | ||
| { | ||
| foreach (var stop in _subscriptionRefCounts.Keys) | ||
| { | ||
| stop.Parent = null; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Logic and Correctness — |
||
| stop.PropertyChanged -= OnGradientStopPropertyChanged; | ||
| } | ||
|
|
||
| _subscriptionRefCounts.Clear(); | ||
| } | ||
|
|
||
| void ResubscribeCollection(GradientStopCollection collection) | ||
| { | ||
| UnsubscribeFromAllGradientStops(); | ||
|
|
||
| if (collection is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| foreach (var stop in collection) | ||
| { | ||
| SubscribeToGradientStop(stop); | ||
| } | ||
| } | ||
|
|
||
| void OnGradientStopPropertyChanged(object sender, PropertyChangedEventArgs e) | ||
|
|
||
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.
[major] Build & MSBuild —
ReferenceEqualityCompareris not public for thenetstandard2.0/netstandard2.1targets thatControls.Core.csprojbuilds. A targeteddotnet build src/Controls/src/Core/Controls.Core.csproj -f netstandard2.0 --no-restorefails here with CS0122, so the package cannot build until this uses a netstandard-compatible reference comparer or explicitReferenceEquals/identity-hash bookkeeping.