Skip to content

Commit de4f1e1

Browse files
committed
Weak subscription to CanExecuteChange events (#29837)
* Weak subscription to CanExecuteChange events * - add tests * - fix lifecycle of CanExecute subscription * - cleanup code based on Pictos recommendations * - add a netstandard_20 version of DependentHandle * - add more * - fix missing null check on textcell
1 parent a59ed2f commit de4f1e1

File tree

12 files changed

+367
-45
lines changed

12 files changed

+367
-45
lines changed

src/Controls/src/Core/Button/Button.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,5 +616,7 @@ private protected override string GetDebuggerDisplay()
616616
var commandText = DebuggerDisplayHelpers.GetDebugText(nameof(Command), Command, false);
617617
return $"{base.GetDebuggerDisplay()}, {textString}, {commandText}";
618618
}
619+
620+
WeakCommandSubscription ICommandElement.CleanupTracker { get; set; }
619621
}
620622
}

src/Controls/src/Core/Cells/TextCell.cs

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,27 @@
11
#nullable disable
22
using System;
33
using System.Windows.Input;
4+
using Microsoft.Maui.Controls.Internals;
45
using Microsoft.Maui.Graphics;
56

67
namespace Microsoft.Maui.Controls
78
{
89
/// <include file="../../../docs/Microsoft.Maui.Controls/TextCell.xml" path="Type[@FullName='Microsoft.Maui.Controls.TextCell']/Docs/*" />
9-
public class TextCell : Cell
10+
public class TextCell : Cell, ICommandElement
1011
{
1112
/// <summary>Bindable property for <see cref="Command"/>.</summary>
12-
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(TextCell), default(ICommand),
13-
propertyChanging: (bindable, oldvalue, newvalue) =>
14-
{
15-
var textCell = (TextCell)bindable;
16-
var oldcommand = (ICommand)oldvalue;
17-
if (oldcommand != null)
18-
oldcommand.CanExecuteChanged -= textCell.OnCommandCanExecuteChanged;
19-
}, propertyChanged: (bindable, oldvalue, newvalue) =>
20-
{
21-
var textCell = (TextCell)bindable;
22-
var newcommand = (ICommand)newvalue;
23-
if (newcommand != null)
24-
{
25-
textCell.IsEnabled = newcommand.CanExecute(textCell.CommandParameter);
26-
newcommand.CanExecuteChanged += textCell.OnCommandCanExecuteChanged;
27-
}
28-
});
13+
public static readonly BindableProperty CommandProperty =
14+
BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(TextCell),
15+
propertyChanging: CommandElement.OnCommandChanging,
16+
propertyChanged: CommandElement.OnCommandChanged);
2917

3018
/// <summary>Bindable property for <see cref="CommandParameter"/>.</summary>
31-
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(TextCell), default(object),
32-
propertyChanged: (bindable, oldvalue, newvalue) =>
33-
{
34-
var textCell = (TextCell)bindable;
35-
if (textCell.Command != null)
36-
{
37-
textCell.IsEnabled = textCell.Command.CanExecute(newvalue);
38-
}
39-
});
19+
public static readonly BindableProperty CommandParameterProperty =
20+
BindableProperty.Create(nameof(CommandParameter),
21+
typeof(object),
22+
typeof(TextCell),
23+
null,
24+
propertyChanged: CommandElement.OnCommandParameterChanged);
4025

4126
/// <summary>Bindable property for <see cref="Text"/>.</summary>
4227
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(TextCell), default(string));
@@ -104,9 +89,14 @@ protected internal override void OnTapped()
10489
Command?.Execute(CommandParameter);
10590
}
10691

107-
void OnCommandCanExecuteChanged(object sender, EventArgs eventArgs)
92+
void ICommandElement.CanExecuteChanged(object sender, EventArgs eventArgs)
10893
{
94+
if (Command is null)
95+
return;
96+
10997
IsEnabled = Command.CanExecute(CommandParameter);
11098
}
99+
100+
WeakCommandSubscription ICommandElement.CleanupTracker { get; set; }
111101
}
112102
}

src/Controls/src/Core/CommandElement.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,24 @@ static class CommandElement
1010
public static void OnCommandChanging(BindableObject bo, object o, object n)
1111
{
1212
var commandElement = (ICommandElement)bo;
13-
if (o is ICommand oldCommand)
14-
oldCommand.CanExecuteChanged -= commandElement.CanExecuteChanged;
13+
commandElement.CleanupTracker?.Dispose();
14+
commandElement.CleanupTracker = null;
1515
}
1616

1717
public static void OnCommandChanged(BindableObject bo, object o, object n)
1818
{
1919
var commandElement = (ICommandElement)bo;
20-
if (n is ICommand newCommand)
21-
newCommand.CanExecuteChanged += commandElement.CanExecuteChanged;
20+
21+
if (n is null)
22+
{
23+
commandElement.CleanupTracker?.Dispose();
24+
commandElement.CleanupTracker = null;
25+
}
26+
else
27+
{
28+
commandElement.CleanupTracker = new WeakCommandSubscription(bo, (ICommand)n, commandElement.CanExecuteChanged);
29+
}
30+
2231
commandElement.CanExecuteChanged(bo, EventArgs.Empty);
2332
}
2433

@@ -36,4 +45,4 @@ public static bool GetCanExecute(ICommandElement commandElement)
3645
return commandElement.Command.CanExecute(commandElement.CommandParameter);
3746
}
3847
}
39-
}
48+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#nullable enable
2+
3+
#if NETSTANDARD2_0 || NETSTANDARD2_1
4+
using System;
5+
using System.Runtime;
6+
using System.Runtime.CompilerServices;
7+
using System.Windows.Input;
8+
9+
namespace System.Runtime;
10+
11+
/// <summary>
12+
/// A wrapper around ConditionalWeakTable that replicates DependentHandle behavior.
13+
/// Creates a dependency between a primary object and a dependent object where
14+
/// the dependent object becomes eligible for collection when the primary is collected.
15+
/// </summary>
16+
internal class DependentHandle : IDisposable
17+
{
18+
private readonly ConditionalWeakTable<object, object> _table;
19+
private readonly WeakReference<object> _primaryRef;
20+
private bool _disposed;
21+
22+
/// <summary>
23+
/// Initializes a new instance of DependentHandle with a primary and dependent object.
24+
/// </summary>
25+
/// <param name="primary">The primary object that controls the lifetime of the dependent object.</param>
26+
/// <param name="dependent">The dependent object that will be collected when primary is collected.</param>
27+
public DependentHandle(object primary, object? dependent)
28+
{
29+
_table = new ConditionalWeakTable<object, object>();
30+
_primaryRef = new WeakReference<object>(primary);
31+
32+
// Store the dependent object in the table, keyed by the primary object
33+
if (dependent is not null)
34+
{
35+
_table.Add(primary, dependent);
36+
}
37+
}
38+
39+
/// <summary>
40+
/// Gets the primary object if it's still alive, otherwise returns null.
41+
/// </summary>
42+
public object? Target
43+
{
44+
get
45+
{
46+
if (_disposed)
47+
return null;
48+
49+
return _primaryRef.TryGetTarget(out var target) ? target : null;
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Gets the dependent object if the primary object is still alive, otherwise returns null.
55+
/// </summary>
56+
public object? Dependent
57+
{
58+
get
59+
{
60+
if (_disposed)
61+
return null;
62+
63+
if (_primaryRef.TryGetTarget(out var primary) &&
64+
_table.TryGetValue(primary, out var dependent))
65+
{
66+
return dependent;
67+
}
68+
69+
return null;
70+
}
71+
}
72+
73+
/// <summary>
74+
/// Checks if both primary and dependent objects are still alive.
75+
/// </summary>
76+
public bool IsAllocated => Target is not null && Dependent is not null;
77+
78+
/// <summary>
79+
/// Disposes the DependentHandleCWT, clearing all references.
80+
/// </summary>
81+
public void Dispose()
82+
{
83+
if (_disposed)
84+
return;
85+
86+
_disposed = true;
87+
88+
// Clear the table - this will allow dependent objects to be collected
89+
// even if the primary object is still alive
90+
if (_primaryRef.TryGetTarget(out var primary))
91+
{
92+
_table.Remove(primary);
93+
}
94+
}
95+
}
96+
#endif

src/Controls/src/Core/ICommandElement.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ interface ICommandElement
1212

1313
// implement these explicitly
1414
void CanExecuteChanged(object? sender, EventArgs e);
15+
16+
WeakCommandSubscription? CleanupTracker { get; set; }
1517
}
1618
}

src/Controls/src/Core/ImageButton/ImageButton.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,5 +288,13 @@ void IButton.Released()
288288
Color IButtonStroke.StrokeColor => (Color)GetValue(BorderColorProperty);
289289

290290
int IButtonStroke.CornerRadius => (int)GetValue(CornerRadiusProperty);
291+
292+
293+
WeakCommandSubscription ICommandElement.CleanupTracker
294+
{
295+
get;
296+
set;
297+
}
298+
291299
}
292300
}

src/Controls/src/Core/Menu/MenuItem.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,5 +198,11 @@ void OnImageSourceSourceChanged(object sender, EventArgs e)
198198
{
199199
OnPropertyChanged(IconImageSourceProperty.PropertyName);
200200
}
201+
202+
WeakCommandSubscription ICommandElement.CleanupTracker
203+
{
204+
get;
205+
set;
206+
}
201207
}
202208
}

src/Controls/src/Core/RefreshView/RefreshView.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,5 +156,11 @@ private protected override string GetDebuggerDisplay()
156156
var debugText = DebuggerDisplayHelpers.GetDebugText(nameof(Command), Command, nameof(IsRefreshing), IsRefreshing, false);
157157
return $"{base.GetDebuggerDisplay()}, {debugText}";
158158
}
159+
160+
WeakCommandSubscription ICommandElement.CleanupTracker
161+
{
162+
get;
163+
set;
164+
}
159165
}
160166
}

src/Controls/src/Core/SearchBar/SearchBar.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,11 @@ private protected override string GetDebuggerDisplay()
160160
var debugText = DebuggerDisplayHelpers.GetDebugText(nameof(SearchCommand), SearchCommand);
161161
return $"{base.GetDebuggerDisplay()}, {debugText}";
162162
}
163+
164+
WeakCommandSubscription ICommandElement.CleanupTracker
165+
{
166+
get;
167+
set;
168+
}
163169
}
164170
}

src/Controls/src/Core/Shell/SearchHandler.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -666,16 +666,16 @@ void ClearPlaceholderCanExecuteChanged(object sender, EventArgs e)
666666
ClearPlaceholderEnabledCore = ClearPlaceholderCommand.CanExecute(ClearPlaceholderCommandParameter);
667667
}
668668

669+
internal WeakCommandSubscription ClearPlaceholderCommandSubscription { get; set; }
670+
669671
void OnClearPlaceholderCommandChanged(ICommand oldCommand, ICommand newCommand)
670672
{
671-
if (oldCommand != null)
672-
{
673-
oldCommand.CanExecuteChanged -= ClearPlaceholderCanExecuteChanged;
674-
}
673+
ClearPlaceholderCommandSubscription?.Dispose();
674+
ClearPlaceholderCommandSubscription = null;
675675

676676
if (newCommand != null)
677677
{
678-
newCommand.CanExecuteChanged += ClearPlaceholderCanExecuteChanged;
678+
ClearPlaceholderCommandSubscription = new WeakCommandSubscription(this, newCommand, ClearPlaceholderCanExecuteChanged);
679679
ClearPlaceholderEnabledCore = ClearPlaceholderCommand.CanExecute(ClearPlaceholderCommandParameter);
680680
}
681681
else
@@ -690,16 +690,16 @@ void OnClearPlaceholderCommandParameterChanged()
690690
ClearPlaceholderEnabledCore = ClearPlaceholderCommand.CanExecute(CommandParameter);
691691
}
692692

693+
internal WeakCommandSubscription CommandSubscription { get; set; }
694+
693695
void OnCommandChanged(ICommand oldCommand, ICommand newCommand)
694696
{
695-
if (oldCommand != null)
696-
{
697-
oldCommand.CanExecuteChanged -= CanExecuteChanged;
698-
}
697+
CommandSubscription?.Dispose();
698+
CommandSubscription = null;
699699

700-
if (newCommand != null)
700+
if (newCommand is not null)
701701
{
702-
newCommand.CanExecuteChanged += CanExecuteChanged;
702+
CommandSubscription = new WeakCommandSubscription(this, newCommand, CanExecuteChanged);
703703
IsSearchEnabledCore = Command.CanExecute(CommandParameter);
704704
}
705705
else

0 commit comments

Comments
 (0)