Skip to content

Commit b8fb2b2

Browse files
committed
Weak subscription to CanExecuteChange events
1 parent cc8b096 commit b8fb2b2

File tree

11 files changed

+149
-49
lines changed

11 files changed

+149
-49
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: 15 additions & 30 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,9 @@ protected internal override void OnTapped()
10489
Command?.Execute(CommandParameter);
10590
}
10691

107-
void OnCommandCanExecuteChanged(object sender, EventArgs eventArgs)
108-
{
92+
void ICommandElement.CanExecuteChanged(object sender, EventArgs eventArgs) =>
10993
IsEnabled = Command.CanExecute(CommandParameter);
110-
}
94+
95+
WeakCommandSubscription ICommandElement.CleanupTracker { get; set; }
11196
}
11297
}

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+
}

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
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#nullable enable
2+
using System;
3+
using System.Windows.Input;
4+
using Microsoft.Maui.Controls.Internals;
5+
6+
namespace Microsoft.Maui.Controls
7+
{
8+
class WeakCommandSubscription : IDisposable
9+
{
10+
internal CommandCanExecuteSubscription Proxy { get; }
11+
public WeakCommandSubscription(
12+
BindableObject bindableObject,
13+
ICommand command,
14+
Action<object, EventArgs> canExecuteChangedHandler)
15+
{
16+
Proxy = new CommandCanExecuteSubscription(bindableObject, command, canExecuteChangedHandler);
17+
}
18+
19+
~WeakCommandSubscription()
20+
{
21+
Dispose(false);
22+
}
23+
public void Dispose()
24+
{
25+
Dispose(true);
26+
GC.SuppressFinalize(this);
27+
}
28+
29+
protected virtual void Dispose(bool disposing)
30+
{
31+
Proxy.Dispose();
32+
}
33+
34+
internal class CommandCanExecuteSubscription : IDisposable
35+
{
36+
WeakReference<BindableObject> _bindableObject;
37+
WeakReference<Action<object, EventArgs>> _canExecuteChangedHandler;
38+
ICommand? _command;
39+
40+
public CommandCanExecuteSubscription(
41+
BindableObject bindableObject,
42+
ICommand command,
43+
Action<object, EventArgs> canExecuteChangedHandler)
44+
{
45+
_command = command;
46+
_bindableObject = new WeakReference<BindableObject>(bindableObject);
47+
_canExecuteChangedHandler = new WeakReference<Action<object, EventArgs>>(canExecuteChangedHandler);
48+
_command.CanExecuteChanged += CanExecuteChanged;
49+
}
50+
51+
public void Dispose()
52+
{
53+
if (_command is not null)
54+
{
55+
_command.CanExecuteChanged -= CanExecuteChanged;
56+
_command = null;
57+
}
58+
}
59+
60+
void CanExecuteChanged(object? arg1, EventArgs args)
61+
{
62+
if (_bindableObject is not null && _bindableObject.TryGetTarget(out var bindableObject) &&
63+
_canExecuteChangedHandler is not null && _canExecuteChangedHandler.TryGetTarget(out var canExecuteChangedHandler))
64+
{
65+
canExecuteChangedHandler(bindableObject, args);
66+
}
67+
else
68+
{
69+
Dispose();
70+
}
71+
}
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)