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
19 changes: 19 additions & 0 deletions src/Controls/src/Core/BindableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,25 @@ internal bool GetIsBound(BindableProperty targetProperty)
return bpcontext != null && bpcontext.Bindings.Count > 0;
}

/// <summary>
/// Forces the binding for the specified property to apply immediately.
/// This is used when one property depends on another and needs the dependent
/// property's binding to resolve before proceeding.
/// See https://github.com/dotnet/maui/issues/31939
/// </summary>
internal void ForceBindingApply(BindableProperty targetProperty)
{
if (targetProperty == null)
throw new ArgumentNullException(nameof(targetProperty));

BindablePropertyContext bpcontext = GetContext(targetProperty);
if (bpcontext == null || bpcontext.Bindings.Count == 0)
return;

// Force the binding to apply now
ApplyBinding(bpcontext, fromBindingContextChanged: false);
}

internal virtual void OnRemoveDynamicResource(BindableProperty property)
{
}
Expand Down
15 changes: 15 additions & 0 deletions src/Controls/src/Core/BindableProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,21 @@ public sealed class BindableProperty

internal ValidateValueDelegate ValidateValue { get; private set; }

// Properties that this property depends on - when getting this property's value,
// if the dependency has a pending binding, return the default value instead.
// This is used to fix timing issues where one property binding resolves before another.
// See https://github.com/dotnet/maui/issues/31939
internal BindableProperty[] Dependencies { get; private set; }

/// <summary>
/// Registers a dependency on another BindableProperty. When this property's value is retrieved,
/// if the dependency has a binding that hasn't resolved yet (value is null), return null.
/// </summary>
internal void DependsOn(params BindableProperty[] dependencies)
{
Dependencies = dependencies;
}

/// <summary>Creates a new instance of the BindableProperty class.</summary>
/// <param name="propertyName">The name of the BindableProperty.</param>
/// <param name="returnType">The type of the property.</param>
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/Button/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ void ICommandElement.CanExecuteChanged(object sender, EventArgs e) =>
RefreshIsEnabledProperty();

protected override bool IsEnabledCore =>
base.IsEnabledCore && CommandElement.GetCanExecute(this);
base.IsEnabledCore && CommandElement.GetCanExecute(this, CommandProperty);

bool _wasImageLoading;

Expand Down
23 changes: 17 additions & 6 deletions src/Controls/src/Core/Button/ButtonElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,27 @@ static class ButtonElement
/// <summary>
/// The backing store for the <see cref="ICommandElement.Command" /> bindable property.
/// </summary>
public static readonly BindableProperty CommandProperty = BindableProperty.Create(
nameof(IButtonElement.Command), typeof(ICommand), typeof(IButtonElement), null,
propertyChanging: CommandElement.OnCommandChanging, propertyChanged: CommandElement.OnCommandChanged);
public static readonly BindableProperty CommandProperty;

/// <summary>
/// The backing store for the <see cref="ICommandElement.CommandParameter" /> bindable property.
/// </summary>
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(
nameof(IButtonElement.CommandParameter), typeof(object), typeof(IButtonElement), null,
propertyChanged: CommandElement.OnCommandParameterChanged);
public static readonly BindableProperty CommandParameterProperty;

static ButtonElement()
{
CommandParameterProperty = BindableProperty.Create(
nameof(IButtonElement.CommandParameter), typeof(object), typeof(IButtonElement), null,
propertyChanged: CommandElement.OnCommandParameterChanged);

CommandProperty = BindableProperty.Create(
nameof(IButtonElement.Command), typeof(ICommand), typeof(IButtonElement), null,
propertyChanging: CommandElement.OnCommandChanging, propertyChanged: CommandElement.OnCommandChanged);

// Register dependency: Command depends on CommandParameter for CanExecute evaluation
// See https://github.com/dotnet/maui/issues/31939
CommandProperty.DependsOn(CommandParameterProperty);
}

/// <summary>
/// The string identifier for the pressed visual state of this control.
Expand Down
26 changes: 16 additions & 10 deletions src/Controls/src/Core/Cells/TextCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,28 @@ namespace Microsoft.Maui.Controls
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),
propertyChanging: CommandElement.OnCommandChanging,
propertyChanged: CommandElement.OnCommandChanged);
public static readonly BindableProperty CommandProperty;

/// <summary>Bindable property for <see cref="CommandParameter"/>.</summary>
public static readonly BindableProperty CommandParameterProperty =
BindableProperty.Create(nameof(CommandParameter),
public static readonly BindableProperty CommandParameterProperty;

static TextCell()
{
CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter),
typeof(object),
typeof(TextCell),
null,
propertyChanged: CommandElement.OnCommandParameterChanged);

CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(TextCell),
propertyChanging: CommandElement.OnCommandChanging,
propertyChanged: CommandElement.OnCommandChanged);

// Register dependency: Command depends on CommandParameter for CanExecute evaluation
// See https://github.com/dotnet/maui/issues/31939
CommandProperty.DependsOn(CommandParameterProperty);
}

/// <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 @@ -95,10 +104,7 @@ protected internal override void OnTapped()

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

IsEnabled = Command.CanExecute(CommandParameter);
IsEnabled = CommandElement.GetCanExecute(this, CommandProperty);
}

WeakCommandSubscription ICommandElement.CleanupTracker { get; set; }
Expand Down
8 changes: 7 additions & 1 deletion src/Controls/src/Core/CheckBox/CheckBox.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ namespace Microsoft.Maui.Controls
{
public partial class CheckBox
{
static CheckBox() => RemapForControls();
static CheckBox()
{
// Register dependency: Command depends on CommandParameter for CanExecute evaluation
// See https://github.com/dotnet/maui/issues/31939
CommandProperty.DependsOn(CommandParameterProperty);
RemapForControls();
}

private new static void RemapForControls()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/CheckBox/CheckBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ void ICommandElement.CanExecuteChanged(object sender, EventArgs e) =>
RefreshIsEnabledProperty();

protected override bool IsEnabledCore =>
base.IsEnabledCore && CommandElement.GetCanExecute(this);
base.IsEnabledCore && CommandElement.GetCanExecute(this, CommandProperty);
public Paint Foreground => Color?.AsPaint();

bool ICheckBox.IsChecked
Expand Down
14 changes: 13 additions & 1 deletion src/Controls/src/Core/CommandElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,23 @@ public static void OnCommandParameterChanged(BindableObject bo, object o, object
commandElement.CanExecuteChanged(bo, EventArgs.Empty);
}

public static bool GetCanExecute(ICommandElement commandElement)
public static bool GetCanExecute(ICommandElement commandElement, BindableProperty? commandProperty = null)
{
if (commandElement.Command == null)
return true;

// If there are dependencies (e.g., CommandParameter for Command), force their bindings
// to apply before evaluating CanExecute. This fixes timing issues where Command binding
// resolves before CommandParameter binding during reparenting.
// See https://github.com/dotnet/maui/issues/31939
if (commandProperty?.Dependencies is not null && commandElement is BindableObject bo)
{
foreach (var dependency in commandProperty.Dependencies)
{
bo.ForceBindingApply(dependency);
}
}

return commandElement.Command.CanExecute(commandElement.CommandParameter);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/ImageButton/ImageButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ bool IImageElement.IsAnimationPlaying
bool IImageController.GetLoadAsAnimation() => false;

protected override bool IsEnabledCore =>
base.IsEnabledCore && CommandElement.GetCanExecute(this);
base.IsEnabledCore && CommandElement.GetCanExecute(this, CommandProperty);

void ICommandElement.CanExecuteChanged(object sender, EventArgs e) =>
RefreshIsEnabledProperty();
Expand Down
27 changes: 19 additions & 8 deletions src/Controls/src/Core/Menu/MenuItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,26 @@ namespace Microsoft.Maui.Controls
public partial class MenuItem : BaseMenuItem, IMenuItemController, ICommandElement, IMenuElement, IPropertyPropagationController
{
/// <summary>Bindable property for <see cref="Command"/>.</summary>
public static readonly BindableProperty CommandProperty = BindableProperty.Create(
nameof(Command), typeof(ICommand), typeof(MenuItem), null,
propertyChanging: CommandElement.OnCommandChanging,
propertyChanged: CommandElement.OnCommandChanged);
public static readonly BindableProperty CommandProperty;

/// <summary>Bindable property for <see cref="CommandParameter"/>.</summary>
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(
nameof(CommandParameter), typeof(object), typeof(MenuItem), null,
propertyChanged: CommandElement.OnCommandParameterChanged);
public static readonly BindableProperty CommandParameterProperty;

static MenuItem()
{
CommandParameterProperty = BindableProperty.Create(
nameof(CommandParameter), typeof(object), typeof(MenuItem), null,
propertyChanged: CommandElement.OnCommandParameterChanged);

CommandProperty = BindableProperty.Create(
nameof(Command), typeof(ICommand), typeof(MenuItem), null,
propertyChanging: CommandElement.OnCommandChanging,
propertyChanged: CommandElement.OnCommandChanged);

// Register dependency: Command depends on CommandParameter for CanExecute evaluation
// See https://github.com/dotnet/maui/issues/31939
CommandProperty.DependsOn(CommandParameterProperty);
}

/// <summary>Bindable property for <see cref="IsDestructive"/>.</summary>
public static readonly BindableProperty IsDestructiveProperty = BindableProperty.Create(nameof(IsDestructive), typeof(bool), typeof(MenuItem), false);
Expand Down Expand Up @@ -122,7 +133,7 @@ static object CoerceIsEnabledProperty(BindableObject bindable, object value)
return false;
}

var canExecute = CommandElement.GetCanExecute(menuItem);
var canExecute = CommandElement.GetCanExecute(menuItem, CommandProperty);
if (!canExecute)
{
return false;
Expand Down
7 changes: 7 additions & 0 deletions src/Controls/src/Core/RefreshView/RefreshView.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ namespace Microsoft.Maui.Controls
{
public partial class RefreshView
{
static RefreshView()
{
// Register dependency: Command depends on CommandParameter for CanExecute evaluation
// See https://github.com/dotnet/maui/issues/31939
CommandProperty.DependsOn(CommandParameterProperty);
}

internal static new void RemapForControls()
{
// Adjust the mappings to preserve Controls.RefreshView legacy behaviors
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/RefreshView/RefreshView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ static object CoerceIsRefreshEnabledProperty(BindableObject bindable, object val
if (bindable is RefreshView refreshView)
{
refreshView._isRefreshEnabledExplicit = (bool)value;
return refreshView._isRefreshEnabledExplicit && CommandElement.GetCanExecute(refreshView);
return refreshView._isRefreshEnabledExplicit && CommandElement.GetCanExecute(refreshView, CommandProperty);
}

return false;
Expand Down
7 changes: 7 additions & 0 deletions src/Controls/src/Core/SearchBar/SearchBar.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ namespace Microsoft.Maui.Controls
{
public partial class SearchBar
{
static SearchBar()
{
// Register dependency: SearchCommand depends on SearchCommandParameter for CanExecute evaluation
// See https://github.com/dotnet/maui/issues/31939
SearchCommandProperty.DependsOn(SearchCommandParameterProperty);
}

internal static new void RemapForControls()
{
// Adjust the mappings to preserve Controls.SearchBar legacy behaviors
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/SearchBar/SearchBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ private void OnRequestedThemeChanged(object sender, AppThemeChangedEventArgs e)
object ICommandElement.CommandParameter => SearchCommandParameter;

protected override bool IsEnabledCore =>
base.IsEnabledCore && CommandElement.GetCanExecute(this);
base.IsEnabledCore && CommandElement.GetCanExecute(this, SearchCommandProperty);

void ICommandElement.CanExecuteChanged(object sender, EventArgs e) =>
RefreshIsEnabledProperty();
Expand Down
20 changes: 20 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui31939.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui31939">
<local:Maui31939Control x:Name="TestControl"
TestCommand="{Binding TestCommand}"
TestCommandParameter="TestValue">
<local:Maui31939Control.ControlTemplate>
<ControlTemplate>
<Grid x:Name="MainLayout">
<Button x:Name="TestButton"
Text="Test"
Command="{TemplateBinding TestCommand}"
CommandParameter="{TemplateBinding TestCommandParameter}" />
</Grid>
</ControlTemplate>
</local:Maui31939Control.ControlTemplate>
</local:Maui31939Control>
</ContentPage>
Loading
Loading