Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 136 additions & 35 deletions src/Controls/src/Core/GradientBrush.cs
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;

Expand All @@ -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

View check run for this annotation

Azure Pipelines / maui-pr (Pack .NET MAUI Pack macOS)

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs

View check run for this annotation

Azure Pipelines / maui-pr (Pack .NET MAUI Pack macOS)

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs

View check run for this annotation

Azure Pipelines / maui-pr (Pack .NET MAUI Pack macOS)

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs

View check run for this annotation

Azure Pipelines / maui-pr (Pack .NET MAUI Pack macOS)

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs

View check run for this annotation

Azure Pipelines / maui-pr (Build .NET MAUI Build macOS (Release))

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs

View check run for this annotation

Azure Pipelines / maui-pr (Build .NET MAUI Build macOS (Release))

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs

View check run for this annotation

Azure Pipelines / maui-pr (Build .NET MAUI Build macOS (Debug))

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs

View check run for this annotation

Azure Pipelines / maui-pr (Build .NET MAUI Build macOS (Debug))

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs

View check run for this annotation

Azure Pipelines / maui-pr

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

Check failure on line 13 in src/Controls/src/Core/GradientBrush.cs

View check run for this annotation

Azure Pipelines / maui-pr

src/Controls/src/Core/GradientBrush.cs#L13

src/Controls/src/Core/GradientBrush.cs(13,71): error CS0122: (NETCORE_ENGINEERING_TELEMETRY=Build) 'ReferenceEqualityComparer' is inaccessible due to its protection level

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.

🔍 AI-Generated Review (multi-model)

[major] Build & MSBuild — ReferenceEqualityComparer is not public for the netstandard2.0/netstandard2.1 targets that Controls.Core.csproj builds. A targeted dotnet build src/Controls/src/Core/Controls.Core.csproj -f netstandard2.0 --no-restore fails here with CS0122, so the package cannot build until this uses a netstandard-compatible reference comparer or explicit ReferenceEquals/identity-hash bookkeeping.


/// <summary>Initializes a new instance of the <see cref="GradientBrush"/> class.</summary>
public GradientBrush()
{
Expand Down Expand Up @@ -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;

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.

🔍 AI-Generated Review (multi-model)

[moderate] Logic and Correctness — Clear()/Reset now nulls every tracked stop's Parent even if another brush currently owns the same shared GradientStop. Repro: add one stop to brush1, then brush2, then call brush1.GradientStops.Clear(); this Reset path clears stop.Parent from brush2 to null, breaking inherited context/parent state for brush2. Only clear the parent when ReferenceEquals(stop.Parent, this).

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)
Expand Down
28 changes: 28 additions & 0 deletions src/Controls/tests/Core.UnitTests/LinearGradientBrushTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,5 +237,33 @@ public void TestHasTransparencyLinearGradientBrush()
};
Assert.True(Brush.HasTransparency(allSemiTransparentBrush));
}

[Fact]
public void ClearUnsubscribesPreviousGradientStopsFromPropertyChanged()
Comment thread
devanathan-vaithiyanathan marked this conversation as resolved.
{
var brush = new LinearGradientBrush();
var oldStop = new GradientStop { Color = Colors.Red, Offset = 0.0f };
var newStop = new GradientStop { Color = Colors.Blue, Offset = 1.0f };
var invalidations = 0;

brush.InvalidateGradientBrushRequested += (_, _) => invalidations++;

brush.GradientStops.Add(oldStop);
invalidations = 0;

brush.GradientStops.Clear();
invalidations = 0;

// If Reset handling does not unsubscribe old stops, mutating oldStop
// would incorrectly invalidate the brush.
oldStop.Color = Colors.Green;
Assert.Equal(0, invalidations);

brush.GradientStops.Add(newStop);
invalidations = 0;

newStop.Color = Colors.Purple;
Assert.Equal(1, invalidations);
}
}
}
Loading