Skip to content
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

Propagate VisualElement.IsEnabled to children #12488

Merged
merged 13 commits into from
Jan 17, 2023
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?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"
x:Class="Maui.Controls.Sample.Pages.LayoutIsEnabledPage"
Title="Layout IsEnabled">

<ContentPage.Resources>
<ResourceDictionary>
<Style TargetType="VerticalStackLayout">
<Setter Property="Padding" Value="6" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>

<ScrollView>
<Grid ColumnSpacing="6" RowSpacing="6" ColumnDefinitions="*,*" RowDefinitions="Auto,Auto">

<VerticalStackLayout x:Name="MainLayout">

<Label Text="All children are enabled" />
<VerticalStackLayout Background="LightBlue">
<Button Text="Enabled" />
<Button Text="Enabled" />
</VerticalStackLayout>

<Label Text="All children are disabled" />
<VerticalStackLayout Background="LightBlue">
<Button Text="Disabled" IsEnabled="False" />
<Button Text="Disabled" IsEnabled="False" />
</VerticalStackLayout>

<Label Text="All children are disabled (because layout is disabled)" />
<VerticalStackLayout IsEnabled="False" Background="LightPink">
<Button Text="Disabled" />
<Button Text="Disabled" />
</VerticalStackLayout>

<Label Text="First item is enabled and the second one is disabled" />
<VerticalStackLayout Background="LightSeaGreen">
<Button Text="Enabled" />
<Button x:Name="DisabledButton" Text="Disabled" IsEnabled="False" />
</VerticalStackLayout>

<Label Text="Children have commands attached" />
<VerticalStackLayout Background="LightSeaGreen">
<Button Text="Enabled" Command="{Binding TheCommand}" />
<Button x:Name="DisabledCommandButton" Text="Disabled" IsEnabled="False" Command="{Binding TheCommand}" />
</VerticalStackLayout>

<Label Text="Nested layouts" />
<VerticalStackLayout Background="LightSkyBlue">
<VerticalStackLayout Background="LightGray">
<Button Text="Enabled" />
<Button Text="Disabled" IsEnabled="False" />
</VerticalStackLayout>
</VerticalStackLayout>

</VerticalStackLayout>

<VerticalStackLayout Grid.Row="1">
<Button Text="Disable Layout" Clicked="OnDisableLayoutBtnClicked" />
<Button Text="Enable Button" Clicked="OnDisableButtonBtnClicked" />
</VerticalStackLayout>

<VerticalStackLayout IsEnabled="{Binding IsLayoutEnabled}" Grid.Column="1">

<Label Text="All children are enabled" />
<VerticalStackLayout Background="LightBlue">
<Button Text="Enabled" />
<Button Text="Enabled" />
</VerticalStackLayout>

<Label Text="All children are disabled" />
<VerticalStackLayout Background="LightBlue">
<Button Text="Disabled" IsEnabled="False" />
<Button Text="Disabled" IsEnabled="False" />
</VerticalStackLayout>

<Label Text="All children are disabled (because layout is disabled)" />
<VerticalStackLayout IsEnabled="False" Background="LightPink">
<Button Text="Disabled" />
<Button Text="Disabled" />
</VerticalStackLayout>

<Label Text="First item is enabled and the second one is disabled" />
<VerticalStackLayout Background="LightSeaGreen">
<Button Text="Enabled" />
<Button IsEnabled="{Binding IsButtonEnabled}" Text="Disabled" />
</VerticalStackLayout>

<Label Text="Children have commands attached" />
<VerticalStackLayout Background="LightSeaGreen">
<Button Text="Enabled" Command="{Binding TheCommand}" />
<Button Text="Disabled" IsEnabled="{Binding IsButtonEnabled}" Command="{Binding TheCommand}" />
</VerticalStackLayout>

<Label Text="Nested layouts" />
<VerticalStackLayout Background="LightSkyBlue">
<VerticalStackLayout Background="LightGray">
<Button Text="Enabled" />
<Button Text="Disabled" IsEnabled="False" />
</VerticalStackLayout>
</VerticalStackLayout>

</VerticalStackLayout>

<VerticalStackLayout Grid.Row="1" Grid.Column="1">
<Label Text="Enable/Disable Layout" />
<CheckBox IsChecked="{Binding IsLayoutEnabled}" />
<Label Text="Enable/Disable Button" />
<CheckBox IsChecked="{Binding IsButtonEnabled}" />
<Label Text="Enable/Disable Command" />
<CheckBox IsChecked="{Binding IsCommandEnabled}" />
</VerticalStackLayout>

</Grid>
</ScrollView>

</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using Microsoft.Maui.Controls;

namespace Maui.Controls.Sample.Pages
{
public partial class LayoutIsEnabledPage : ContentPage
{
bool isLayoutEnabled;
bool isButtonEnabled;
bool isCommandEnabled;

public LayoutIsEnabledPage()
{
InitializeComponent();

TheCommand = new Command(OnThe, OnTheCanExecute);
BindingContext = this;
}

public bool IsLayoutEnabled
{
get => isLayoutEnabled;
set
{
isLayoutEnabled = value;
OnPropertyChanged();
}
}

public bool IsButtonEnabled
{
get => isButtonEnabled;
set
{
isButtonEnabled = value;
OnPropertyChanged();
}
}

public bool IsCommandEnabled
{
get => isCommandEnabled;
set
{
isCommandEnabled = value;
OnPropertyChanged();
TheCommand.ChangeCanExecute();
}
}

public Command TheCommand { get; }

void OnDisableLayoutBtnClicked(object sender, EventArgs e)
{
MainLayout.IsEnabled = !MainLayout.IsEnabled;

((Button)sender).Text = MainLayout.IsEnabled ? "Disable Layout" : "Enable Layout";
}

void OnDisableButtonBtnClicked(object sender, EventArgs e)
{
DisabledButton.IsEnabled = !DisabledButton.IsEnabled;
DisabledCommandButton.IsEnabled = !DisabledCommandButton.IsEnabled;

DisabledButton.Text = DisabledButton.IsEnabled ? "Enabled" : "Disabled";
DisabledCommandButton.Text = DisabledCommandButton.IsEnabled ? "Enabled" : "Disabled";

((Button)sender).Text = DisabledButton.IsEnabled ? "Disable Button" : "Enable Button";
}

void OnThe()
{
System.Diagnostics.Debug.WriteLine("On THE clicked!");
}

bool OnTheCanExecute() => isCommandEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]

new SectionModel(typeof(CustomLayoutPage), "Custom Layout",
"Demonstrations of custom layout."),

new SectionModel(typeof(LayoutIsEnabledPage), "Layout IsEnabled",
"Demonstrations of enabling/disabling a layout."),
};
}
}
16 changes: 7 additions & 9 deletions src/Controls/src/Core/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.Maui.Controls
/// <summary>
/// A button <see cref="View" /> that reacts to touch events.
/// </summary>
public partial class Button : View, IFontElement, ITextElement, IBorderElement, IButtonController, IElementConfiguration<Button>, IPaddingElement, IImageController, IViewController, IButtonElement, IImageElement
public partial class Button : View, IFontElement, ITextElement, IBorderElement, IButtonController, IElementConfiguration<Button>, IPaddingElement, IImageController, IViewController, IButtonElement, ICommandElement, IImageElement
{
const double DefaultSpacing = 10;

Expand Down Expand Up @@ -240,11 +240,6 @@ public double CharacterSpacing
set { SetValue(TextElement.CharacterSpacingProperty, value); }
}

bool IButtonElement.IsEnabledCore
{
set { SetValueCore(IsEnabledProperty, value); }
}

/// <summary>
/// Internal method to trigger the <see cref="Clicked"/> event.
/// Should not be called manually outside of .NET MAUI.
Expand Down Expand Up @@ -439,9 +434,6 @@ void IBorderElement.OnBorderColorPropertyChanged(Color oldValue, Color newValue)
void IImageElement.OnImageSourceSourceChanged(object sender, EventArgs e) =>
ImageElement.ImageSourceSourceChanged(this, e);

void IButtonElement.OnCommandCanExecuteChanged(object sender, EventArgs e) =>
ButtonElement.CommandCanExecuteChanged(this, EventArgs.Empty);

void IImageController.SetIsLoading(bool isLoading)
{
}
Expand All @@ -465,6 +457,12 @@ void ITextElement.OnTextTransformChanged(TextTransform oldValue, TextTransform n
public virtual string UpdateFormsText(string source, TextTransform textTransform)
=> TextTransformUtilites.GetTransformedText(source, textTransform);

void ICommandElement.CanExecuteChanged(object sender, EventArgs e) =>
RefreshIsEnabledProperty();

internal override bool IsEnabledCore =>
PureWeen marked this conversation as resolved.
Show resolved Hide resolved
base.IsEnabledCore && CommandElement.GetCanExecute(this);

/// <summary>
/// Represents the layout of the button content whenever an image is shown.
/// </summary>
Expand Down
64 changes: 9 additions & 55 deletions src/Controls/src/Core/ButtonElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,27 @@ namespace Microsoft.Maui.Controls
static class ButtonElement
{
/// <summary>
/// The backing store for the <see cref="IButtonElement.Command" /> bindable property.
/// 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: OnCommandChanging, propertyChanged: OnCommandChanged);
public static readonly BindableProperty CommandProperty = BindableProperty.Create(
nameof(IButtonElement.Command), typeof(ICommand), typeof(IButtonElement), null,
propertyChanging: CommandElement.OnCommandChanging, propertyChanged: CommandElement.OnCommandChanged);

/// <summary>
/// The backing store for the <see cref="IButtonElement.CommandParameter" /> bindable property.
/// 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: (bindable, oldvalue, newvalue) => CommandCanExecuteChanged(bindable, EventArgs.Empty));

static void OnCommandChanged(BindableObject bo, object o, object n)
{
IButtonElement button = (IButtonElement)bo;
if (n is ICommand newCommand)
newCommand.CanExecuteChanged += button.OnCommandCanExecuteChanged;

CommandChanged(button);
}

static void OnCommandChanging(BindableObject bo, object o, object n)
{
IButtonElement button = (IButtonElement)bo;
if (o != null)
{
(o as ICommand).CanExecuteChanged -= button.OnCommandCanExecuteChanged;
}
}
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(
nameof(IButtonElement.CommandParameter), typeof(object), typeof(IButtonElement), null,
propertyChanged: CommandElement.OnCommandParameterChanged);

/// <summary>
/// The string identifier for the pressed visual state of this control.
/// </summary>
public const string PressedVisualState = "Pressed";

/// <summary>
/// A method to signal that the <see cref="IButtonElement.Command"/> property has been changed.
/// </summary>
/// <param name="sender">The object initiating this event.</param>
public static void CommandChanged(IButtonElement sender)
{
if (sender.Command != null)
{
CommandCanExecuteChanged(sender, EventArgs.Empty);
}
else
{
sender.IsEnabledCore = true;
}
}

/// <summary>
/// A method to signal that the <see cref="Command.CanExecute(object)"/> might have changed and needs to be reevaluated.
/// </summary>
/// <param name="sender">The object initiating this event.</param>
/// <param name="e">Arguments associated with this event.</param>
public static void CommandCanExecuteChanged(object sender, EventArgs e)
{
IButtonElement ButtonElementManager = (IButtonElement)sender;
ICommand cmd = ButtonElementManager.Command;
if (cmd != null)
{
ButtonElementManager.IsEnabledCore = cmd.CanExecute(ButtonElementManager.CommandParameter);
}
}

/// <summary>
/// A method to signal that this element was clicked/tapped.
/// By calling this, the <see cref="IButtonElement.Command"/> and clicked events are triggered.
/// By calling this, the <see cref="ICommandElement.Command"/> and clicked events are triggered.
/// </summary>
/// <param name="visualElement">The element that was interacted with.</param>
/// <param name="ButtonElementManager">The button element implementation to trigger the commands and events on.</param>
Expand Down
58 changes: 58 additions & 0 deletions src/Controls/src/Core/CommandElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#nullable enable
using System;
using System.Windows.Input;
using Microsoft.Maui.Controls.Internals;

namespace Microsoft.Maui.Controls
{
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;
}

public static void OnCommandChanged(BindableObject bo, object o, object n)
{
var commandElement = (ICommandElement)bo;

if (n is ICommand newCommand)
{
newCommand.CanExecuteChanged += commandElement.CanExecuteChanged;
commandElement.CanExecuteChanged(bo, EventArgs.Empty);
}
}

public static void OnCommandParameterChanged(BindableObject bo, object o, object n)
{
var commandElement = (ICommandElement)bo;
commandElement.CanExecuteChanged(bo, EventArgs.Empty);
}

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

return commandElement.Command.CanExecute(commandElement.CommandParameter);
}

public static void RefreshPropertyValue(BindableObject bo, BindableProperty property, object value)
{
var ctx = bo.GetContext(property);
if (ctx?.Binding is not null)
{
// support bound properties
if (!ctx.Attributes.HasFlag(BindableObject.BindableContextAttributes.IsBeingSet))
ctx.Binding.Apply(false);
}
else
{
// support normal/code properties
bo.SetValue(property, value);
}
}
}
}
Loading