I'd like to create a command-driven custom control in Xamarin.Forms. It should act like a normal button, with different states for normal an disabled. Everything works very well, but I can't get the bound command to drive the control's visual state with the CanExecute property of the command. I tried to use ButtonCommandPropertyChanged, but that's not fired when I call ChangeCanExecute() in my viewmodel.
Of course I could introduce another property on the control to change the state, but I think it should be possible to do this with the commands CanExecute status (and should be more elegant as well...)
[IconButton.xaml]
<?xml version="1.0" encoding="UTF-8"?>
<ContentView WidthRequest="180" HeightRequest="90"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="TestStyle.Controls.IconButton">
<ContentView.Content>
<Frame x:Name="MyFrame" Style="{StaticResource StandardFrameStyle}" Margin="5" Padding="10,12,10,10">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BorderColor" Value="Black"></Setter>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="BorderColor" Value="Red"></Setter>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="4*" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<Image x:Name="InnerImage"
Grid.Row="0"
HorizontalOptions="Center"
Aspect="AspectFit"
Margin="0,0,0,1"
HeightRequest="35"/>
<Label x:Name="InnerLabel"
Grid.Row="1"
VerticalOptions="CenterAndExpand"
HorizontalTextAlignment="Center"
TextColor="{StaticResource TextColor}"
FontSize="12"
LineHeight="0.9"
MaxLines="2"/>
</Grid>
</Frame>
</ContentView.Content>
[IconButton.xaml.cs]
using System;
using System.Diagnostics;
using System.Windows.Input;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace TestStyle.Controls
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class IconButton : ContentView
{
public EventHandler Clicked;
public static readonly BindableProperty ButtonTextProperty =
BindableProperty.Create("ButtonText", typeof(string), typeof(IconButton), default(string));
public string ButtonText
{
get => ((string)GetValue(ButtonTextProperty))?.ToUpper();
set => SetValue(ButtonTextProperty, value);
}
public static readonly BindableProperty ButtonIconProperty =
BindableProperty.Create("ButtonIcon", typeof(ImageSource), typeof(IconButton), default(ImageSource));
public ImageSource ButtonIcon
{
get => (ImageSource)GetValue(ButtonIconProperty);
set => SetValue(ButtonIconProperty, value);
}
public static readonly BindableProperty ButtonCommandProperty =
BindableProperty.Create("ButtonCommand", typeof(ICommand), typeof(IconButton), null, BindingMode.Default, null, ButtonCommandPropertyChanged, ButtonCommandPropertyChanged);
private static void ButtonCommandPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
Debug.WriteLine($"oldValue: ${oldvalue}");
Debug.WriteLine($"newValue: ${newvalue}");
}
public ICommand ButtonCommand
{
get => (ICommand)GetValue(ButtonCommandProperty);
set
{
SetValue(ButtonCommandProperty, value);
Debug.WriteLine("ButtonCommand wurde gesetzt!!");
}
}
public static readonly BindableProperty CommandParameterProperty =
BindableProperty.Create("CommandParameter", typeof(object), typeof(IconButton), null);
public object CommandParameter
{
get => (object)GetValue(CommandParameterProperty);
set => SetValue(CommandParameterProperty, value);
}
public IconButton()
{
InitializeComponent();
this.GestureRecognizers.Add(new TapGestureRecognizer
{
Command = new Command(() =>
{
Clicked?.Invoke(this, EventArgs.Empty);
if (ButtonCommand == null) return;
if (ButtonCommand.CanExecute(CommandParameter))
ButtonCommand.Execute(CommandParameter);
})
});
}
}
}
[FirstPage.xaml]
(I delete some irrelevant parts)
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:TestStyle.Controls;assembly=TestStyle"
xmlns:viewModels="clr-namespace:TestStyle.ViewModels;assembly=TestStyle"
x:DataType="viewModels:ExampleViewModel"
mc:Ignorable="d"
x:Class="TestStyle.Views.FirstPage"
BackgroundColor="{StaticResource LightBgColor}"
Title="Test"
NavigationPage.HasNavigationBar="false">
<ContentPage.BindingContext>
<viewModels:ExampleViewModel/>
</ContentPage.BindingContext>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<controls:IconButton Grid.Row="1" Grid.Column="0"
ButtonText="Enable"
ButtonIcon="shuffle_gr.png"
ButtonCommand="{Binding EnableCommand}" />
<controls:IconButton Grid.Row="1" Grid.Column="1"
ButtonText="Disable"
ButtonIcon="clock_gr.png"
ButtonCommand="{Binding DisableCommand}" />
<controls:IconButton Grid.Row="2" Grid.Column="1"
ButtonText="Personal anfordern"
ButtonIcon="select_gr.png"
ButtonCommand="{Binding MyDynamicCommand}" />
<Label Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding StatusMessage}" HorizontalTextAlignment="Center"/>
</Grid>
</ContentPage>
[ExampleViewModel.cs]
(I delete some irrelevant parts)
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using TestStyle.Models;
using TestStyle.Views;
using Xamarin.Forms;
namespace TestStyle.ViewModels
{
public class ExampleViewModel : INotifyPropertyChanged
{
private string _statusMessage;
private bool _buttonState;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string StatusMessage
{
get => _statusMessage;
set
{
if (_statusMessage == value) return;
_statusMessage = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("StatusMessage"));
}
}
public bool ButtonState
{
get
{
return _buttonState;
}
set
{
_buttonState = value;
OnPropertyChanged(nameof(ButtonState));
}
}
public ICommand MyDynamicCommand { get; }
public ICommand EnableCommand { get; }
public ICommand DisableCommand { get; }
public ExampleViewModel()
{
SetButtonState(true);
MyDynamicCommand = new Command(() => StatusMessage = $"This is dynamic at {DateTime.Now.ToLongTimeString()}", () => ButtonState);
EnableCommand = new Command(() => SetButtonState(true));
DisableCommand = new Command(() => SetButtonState(false));
}
private void SetButtonState(bool newState)
{
ButtonState = newState;
var myCmd = ((Command) MyDynamicCommand);
if (myCmd != null)
{
myCmd.ChangeCanExecute(); // I think my control should be notified here!?
}
}
}
}
I don't know how to wire up the change of the CanExecute to my custom control's visual state.
Any help highly apprreciated: :-) Thank you!
Cause:
You set the Command MyDynamicCommand as readonly, so it will never been changed.
Solution:
From your description, I guess that you want to change the border color of the third frame when tapping the other frames, right?
It is not a good design to re-set the command in runtime. Since you had defined the CommandParameter
. You would better binding the value of ButtonState
to it.
<viewModels:IconButton Grid.Row="2" Grid.Column="1"
ButtonText="Personal anfordern"
CommandParameter="{Binding ButtonState}"
ButtonCommand="{Binding MyDynamicCommand}" />
private void SetButtonState(bool newState)
{
ButtonState = newState;
}
public static readonly BindableProperty CommandParameterProperty =
BindableProperty.Create("CommandParameter", typeof(object), typeof(IconButton), null,propertyChanged:(BindableObject bindable, object oldvalue, object newvalue) => {
var iconbutton = bindable as IconButton;
iconbutton.MyFrame.IsEnabled = (bool)newvalue ;
});
OK, I think I finally got it... This mechanism works the following way:
The custom control uses an extended constructor for the command's BindableProperty (notice the BindingPropertyChangedDelegate):
public static readonly BindableProperty ButtonCommandProperty =
BindableProperty.Create("ButtonCommand", typeof(ICommand), typeof(IconButton), null, BindingMode.Default, null, ButtonCommandPropertyChanged);
On initialization of the control the method ButtonCommandPropertyChanged gets invoked, which registers an EventHandler on the ButtonCommand for CanExecute-changes:
private static void ButtonCommandPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
// wird bei Initialisierung des Buttons aufgerufen
if (!(newvalue is Command)) return;
var buttonCommand = (ICommand) newvalue;
var iconButton = (IconButton) bindable;
iconButton.ChangeEnabledState(buttonCommand.CanExecute(iconButton.ButtonCommandParameter));
buttonCommand.CanExecuteChanged += (sender, args) =>
IconButton_CanExecuteChanged(sender, new BindableObjectEventArgs(bindable));
}
private static void IconButton_CanExecuteChanged(object sender, BindableObjectEventArgs e)
{
// CanExecuteChanged wurde ausgelöst, CanExecute muss erneut ausgewertet werden
var iconButton = (IconButton) e.BindableObject;
var executable = ((ICommand) sender).CanExecute(iconButton.ButtonCommandParameter);
iconButton.ChangeEnabledState(executable);
}
private void ChangeEnabledState(bool state) => VisualStateManager.GoToState(MyFrame, state ? "Normal" : "Disabled");
In my ViewModel I need to inform my command, when the CanExecute has to be reevaluated:
private void SetButtonState(bool newState)
{
ButtonState = newState;
var myCmd = ((Command) MyDynamicCommand);
myCmd?.ChangeCanExecute(); // Command von der Änderung seines CanExecute-Status benachrichtigen (löst CanExecuteChanged-Event aus)
}
That's it. VisualState of the custom control is now controlled by the command's CanExecute status.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.