Skip to content

Weak subscription to CanExecuteChange events #29837

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

Merged
merged 7 commits into from
Jun 17, 2025
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
2 changes: 2 additions & 0 deletions src/Controls/src/Core/Button/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,5 +616,7 @@ private protected override string GetDebuggerDisplay()
var commandText = DebuggerDisplayHelpers.GetDebugText(nameof(Command), Command, false);
return $"{base.GetDebuggerDisplay()}, {textString}, {commandText}";
}

WeakCommandSubscription ICommandElement.CleanupTracker { get; set; }
}
}
46 changes: 18 additions & 28 deletions src/Controls/src/Core/Cells/TextCell.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,27 @@
#nullable disable
using System;
using System.Windows.Input;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;

namespace Microsoft.Maui.Controls
{
/// <include file="../../../docs/Microsoft.Maui.Controls/TextCell.xml" path="Type[@FullName='Microsoft.Maui.Controls.TextCell']/Docs/*" />
public class TextCell : Cell
public class TextCell : Cell, ICommandElement
{
/// <summary>Bindable property for <see cref="Command"/>.</summary>
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(TextCell), default(ICommand),
propertyChanging: (bindable, oldvalue, newvalue) =>
{
var textCell = (TextCell)bindable;
var oldcommand = (ICommand)oldvalue;
if (oldcommand != null)
oldcommand.CanExecuteChanged -= textCell.OnCommandCanExecuteChanged;
}, propertyChanged: (bindable, oldvalue, newvalue) =>
{
var textCell = (TextCell)bindable;
var newcommand = (ICommand)newvalue;
if (newcommand != null)
{
textCell.IsEnabled = newcommand.CanExecute(textCell.CommandParameter);
newcommand.CanExecuteChanged += textCell.OnCommandCanExecuteChanged;
}
});
public static readonly BindableProperty CommandProperty =
BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(TextCell),
propertyChanging: CommandElement.OnCommandChanging,
propertyChanged: CommandElement.OnCommandChanged);

/// <summary>Bindable property for <see cref="CommandParameter"/>.</summary>
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(TextCell), default(object),
propertyChanged: (bindable, oldvalue, newvalue) =>
{
var textCell = (TextCell)bindable;
if (textCell.Command != null)
{
textCell.IsEnabled = textCell.Command.CanExecute(newvalue);
}
});
public static readonly BindableProperty CommandParameterProperty =
BindableProperty.Create(nameof(CommandParameter),
typeof(object),
typeof(TextCell),
null,
propertyChanged: CommandElement.OnCommandParameterChanged);

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

void OnCommandCanExecuteChanged(object sender, EventArgs eventArgs)
void ICommandElement.CanExecuteChanged(object sender, EventArgs eventArgs)
{
if (Command is null)
return;

IsEnabled = Command.CanExecute(CommandParameter);
}

WeakCommandSubscription ICommandElement.CleanupTracker { get; set; }
}
}
19 changes: 14 additions & 5 deletions src/Controls/src/Core/CommandElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,24 @@ static class CommandElement
public static void OnCommandChanging(BindableObject bo, object o, object n)
{
var commandElement = (ICommandElement)bo;
if (o is ICommand oldCommand)
oldCommand.CanExecuteChanged -= commandElement.CanExecuteChanged;
commandElement.CleanupTracker?.Dispose();
commandElement.CleanupTracker = null;
}

public static void OnCommandChanged(BindableObject bo, object o, object n)
{
var commandElement = (ICommandElement)bo;
if (n is ICommand newCommand)
newCommand.CanExecuteChanged += commandElement.CanExecuteChanged;

if (n is null)
{
commandElement.CleanupTracker?.Dispose();
commandElement.CleanupTracker = null;
}
else
{
commandElement.CleanupTracker = new WeakCommandSubscription(bo, (ICommand)n, commandElement.CanExecuteChanged);
}

commandElement.CanExecuteChanged(bo, EventArgs.Empty);
}

Expand All @@ -36,4 +45,4 @@ public static bool GetCanExecute(ICommandElement commandElement)
return commandElement.Command.CanExecute(commandElement.CommandParameter);
}
}
}
}
96 changes: 96 additions & 0 deletions src/Controls/src/Core/DepdendentHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#nullable enable

#if NETSTANDARD2_0 || NETSTANDARD2_1
using System;
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace System.Runtime;

/// <summary>
/// A wrapper around ConditionalWeakTable that replicates DependentHandle behavior.
/// Creates a dependency between a primary object and a dependent object where
/// the dependent object becomes eligible for collection when the primary is collected.
/// </summary>
internal class DependentHandle : IDisposable
{
private readonly ConditionalWeakTable<object, object> _table;
private readonly WeakReference<object> _primaryRef;
private bool _disposed;

/// <summary>
/// Initializes a new instance of DependentHandle with a primary and dependent object.
/// </summary>
/// <param name="primary">The primary object that controls the lifetime of the dependent object.</param>
/// <param name="dependent">The dependent object that will be collected when primary is collected.</param>
public DependentHandle(object primary, object? dependent)
{
_table = new ConditionalWeakTable<object, object>();
_primaryRef = new WeakReference<object>(primary);

// Store the dependent object in the table, keyed by the primary object
if (dependent is not null)
{
_table.Add(primary, dependent);
}
}

/// <summary>
/// Gets the primary object if it's still alive, otherwise returns null.
/// </summary>
public object? Target
{
get
{
if (_disposed)
return null;

return _primaryRef.TryGetTarget(out var target) ? target : null;
}
}

/// <summary>
/// Gets the dependent object if the primary object is still alive, otherwise returns null.
/// </summary>
public object? Dependent
{
get
{
if (_disposed)
return null;

if (_primaryRef.TryGetTarget(out var primary) &&
_table.TryGetValue(primary, out var dependent))
{
return dependent;
}

return null;
}
}

/// <summary>
/// Checks if both primary and dependent objects are still alive.
/// </summary>
public bool IsAllocated => Target is not null && Dependent is not null;

/// <summary>
/// Disposes the DependentHandleCWT, clearing all references.
/// </summary>
public void Dispose()
{
if (_disposed)
return;

_disposed = true;

// Clear the table - this will allow dependent objects to be collected
// even if the primary object is still alive
if (_primaryRef.TryGetTarget(out var primary))
{
_table.Remove(primary);
}
}
}
#endif
2 changes: 2 additions & 0 deletions src/Controls/src/Core/ICommandElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ interface ICommandElement

// implement these explicitly
void CanExecuteChanged(object? sender, EventArgs e);

WeakCommandSubscription? CleanupTracker { get; set; }
}
}
8 changes: 8 additions & 0 deletions src/Controls/src/Core/ImageButton/ImageButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,13 @@ void IButton.Released()
Color IButtonStroke.StrokeColor => (Color)GetValue(BorderColorProperty);

int IButtonStroke.CornerRadius => (int)GetValue(CornerRadiusProperty);


WeakCommandSubscription ICommandElement.CleanupTracker
{
get;
set;
}

}
}
6 changes: 6 additions & 0 deletions src/Controls/src/Core/Menu/MenuItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,11 @@ void OnImageSourceSourceChanged(object sender, EventArgs e)
{
OnPropertyChanged(IconImageSourceProperty.PropertyName);
}

WeakCommandSubscription ICommandElement.CleanupTracker
{
get;
set;
}
}
}
6 changes: 6 additions & 0 deletions src/Controls/src/Core/RefreshView/RefreshView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,11 @@ private protected override string GetDebuggerDisplay()
var debugText = DebuggerDisplayHelpers.GetDebugText(nameof(Command), Command, nameof(IsRefreshing), IsRefreshing, false);
return $"{base.GetDebuggerDisplay()}, {debugText}";
}

WeakCommandSubscription ICommandElement.CleanupTracker
{
get;
set;
}
}
}
6 changes: 6 additions & 0 deletions src/Controls/src/Core/SearchBar/SearchBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,11 @@ private protected override string GetDebuggerDisplay()
var debugText = DebuggerDisplayHelpers.GetDebugText(nameof(SearchCommand), SearchCommand);
return $"{base.GetDebuggerDisplay()}, {debugText}";
}

WeakCommandSubscription ICommandElement.CleanupTracker
{
get;
set;
}
}
}
22 changes: 11 additions & 11 deletions src/Controls/src/Core/Shell/SearchHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -666,16 +666,16 @@ void ClearPlaceholderCanExecuteChanged(object sender, EventArgs e)
ClearPlaceholderEnabledCore = ClearPlaceholderCommand.CanExecute(ClearPlaceholderCommandParameter);
}

internal WeakCommandSubscription ClearPlaceholderCommandSubscription { get; set; }

void OnClearPlaceholderCommandChanged(ICommand oldCommand, ICommand newCommand)
{
if (oldCommand != null)
{
oldCommand.CanExecuteChanged -= ClearPlaceholderCanExecuteChanged;
}
ClearPlaceholderCommandSubscription?.Dispose();
ClearPlaceholderCommandSubscription = null;

if (newCommand != null)
{
newCommand.CanExecuteChanged += ClearPlaceholderCanExecuteChanged;
ClearPlaceholderCommandSubscription = new WeakCommandSubscription(this, newCommand, ClearPlaceholderCanExecuteChanged);
ClearPlaceholderEnabledCore = ClearPlaceholderCommand.CanExecute(ClearPlaceholderCommandParameter);
}
else
Expand All @@ -690,16 +690,16 @@ void OnClearPlaceholderCommandParameterChanged()
ClearPlaceholderEnabledCore = ClearPlaceholderCommand.CanExecute(CommandParameter);
}

internal WeakCommandSubscription CommandSubscription { get; set; }

void OnCommandChanged(ICommand oldCommand, ICommand newCommand)
{
if (oldCommand != null)
{
oldCommand.CanExecuteChanged -= CanExecuteChanged;
}
CommandSubscription?.Dispose();
CommandSubscription = null;

if (newCommand != null)
if (newCommand is not null)
{
newCommand.CanExecuteChanged += CanExecuteChanged;
CommandSubscription = new WeakCommandSubscription(this, newCommand, CanExecuteChanged);
IsSearchEnabledCore = Command.CanExecute(CommandParameter);
}
else
Expand Down
Loading
Loading