Reputation: 592
In my .Net MAUI application I have an email verification screen with four entry fields. I listen for TextChanged
events on these fields and move the focus to the next field when the user enters their code. My problem is that this doesn't work for deleting. If the focus is on an empty entry field and the user press backspace I want to move focus to the previous field, but TextChanged
is not fired because the text is not changed.
I have a working solution for Windows where I have subclassed Entry and listen for KeyUp
events from the native view, but I can't figure out create the same functionality for Mac and iOS.
public class MyEntry : Entry
{
public event EventHandler KeyBackspaceUp;
public MyEntry()
{
Loaded += (o, e) =>
{
#if WINDOWS
UIElement? nativeView = Handler?.PlatformView as UIElement;
if (nativeView != null)
{
nativeView.KeyUp += (o, e) =>
{
if(e.Key == Windows.System.VirtualKey.Back)
{
KeyBackspaceUp?.Invoke(this, EventArgs.Empty);
}
};
}
#elif IOS || MACCATALYST
UITextField nativeView = Handler?.PlatformView as UITextField;
if (nativeView != null)
{
// TODO: Add backspace detection for Mac and iOS
}
#endif
};
}
}
Upvotes: 0
Views: 306
Reputation: 25956
Somewhat inspired by FreakyAli's answer is an Entry
workaround.
Basically, if we split Text
into two components, (1) Text
the visual representation, (2) UserText
the actual data the user enters. What we can do is make:
Text = invisChar + UserText
, and/orUserText = Text with invisChar removed
This works by tricking the Entry
to create TextChanged
events even for the empty case:
// EntryWithBackspace.cs
public class EntryWithBackspace : Entry
{
public event EventHandler? BackspacePressed;
public static readonly BindableProperty UserTextProperty = BindableProperty.Create(nameof(UserText), typeof(string), typeof(EntryWithBackspace), string.Empty);
public string UserText
{
get => (string)GetValue(UserTextProperty);
set => SetValue(UserTextProperty, value);
}
//string invisChar = "X"; // use this version for debugging
string invisChar = " ";
public EntryWithBackspace()
{
this.PropertyChanged += async (s, e) =>
{
switch (e.PropertyName)
{
case nameof(Text):
if ((Text ?? "").Length < (invisChar + (UserText ?? "")).Length)
{
BackspacePressed?.Invoke(this, EventArgs.Empty);
Debug.WriteLine("Backspace");
}
if (Text is string text && text.Length >= 1 && text.Substring(0,1) == invisChar)
{
UserText = text.Substring(1);
}
else
{
UserText = Text ?? "";
await Task.Delay(1);
Text = invisChar + UserText;
}
break;
case nameof(UserText):
Text = invisChar + UserText;
break;
}
};
OnPropertyChanged(nameof(UserText));
}
}
To use this component, you will bind to UserText
instead of Text
, e.g.
<local:EntryWithBackspace UserText="{Binding Email}" BackspacePressed="OnBackspacePress" />
Upvotes: 0
Reputation: 16459
I created the FreakyCodeView for this. I am not sure if it's relevant to you in terms of the design, but you can use the code in it:
https://github.com/FreakyAli/Maui.FreakyControls/
<?xml version="1.0" encoding="utf-8" ?>
<ContentView
x:Class="Maui.FreakyControls.FreakyCodeView"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Name="this">
<Grid>
<Entry x:Name="hiddenTextEntry" Opacity="0">
<Entry.FontSize>
<OnPlatform x:TypeArguments="x:Double" Default="18">
<On Platform="Android" Value="18" />
<On Platform="iOS" Value="22" />
</OnPlatform>
</Entry.FontSize>
</Entry>
<HorizontalStackLayout x:Name="CodeItemContainer" Spacing="{Binding ItemSpacing, Source={x:Reference this}}">
<HorizontalStackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped" />
</HorizontalStackLayout.GestureRecognizers>
</HorizontalStackLayout>
</Grid>
</ContentView>
And its code behind
public partial class FreakyCodeView : ContentView
{
#region Events
public event EventHandler<FreakyCodeCompletedEventArgs> CodeEntryCompleted;
#endregion Events
#region Constructor and Initializations
public FreakyCodeView()
{
InitializeComponent();
hiddenTextEntry.TextChanged += FreakyCodeView_TextChanged;
hiddenTextEntry.Focused += HiddenTextEntry_Focused;
hiddenTextEntry.Unfocused += HiddenTextEntry_Unfocused;
Initialize();
}
private void HiddenTextEntry_Unfocused(object sender, FocusEventArgs e)
{
var CodeItemArray = CodeItemContainer.Children.Select(x => x as CodeView).ToList();
for (int i = 0; i < CodeLength; i++)
{
CodeItemArray[i].UnfocusAnimate();
}
}
private void HiddenTextEntry_Focused(object sender, FocusEventArgs e)
{
var length = CodeValue is null ? 0 : CodeValue.Length;
hiddenTextEntry.CursorPosition = length;
var CodeItemArray = CodeItemContainer.Children.Select(x => x as CodeView).ToArray();
if (length == CodeLength)
{
CodeItemArray[length - 1].FocusAnimate();
}
else
{
for (int i = 0; i < CodeLength; i++)
{
if (i == length)
{
CodeItemArray[i].FocusAnimate();
}
else
{
CodeItemArray[i].UnfocusAnimate();
}
}
}
}
#endregion Constructor and Initializations
#region Methods
public void Initialize()
{
hiddenTextEntry.MaxLength = CodeLength;
SetInputType(CodeInputType);
var count = CodeItemContainer.Children.Count;
if (count < CodeLength)
{
int newItemesToAdd = CodeLength - count;
char[] CodeCharsArray = CodeValue.ToCharArray();
for (int i = 1; i <= newItemesToAdd; i++)
{
CodeView container = CreateItem();
CodeItemContainer.Children.Add(container);
if (CodeValue.Length >= CodeLength)
{
container.SetValueWithAnimation(CodeCharsArray[CodeView.DefaultCodeLength + i - 1]);
}
}
}
else if (count > CodeLength)
{
int ItemesToRemove = count - CodeLength;
for (int i = 1; i <= ItemesToRemove; i++)
{
CodeItemContainer.Children.RemoveAt(CodeItemContainer.Children.Count - 1);
}
}
}
public new void Focus()
{
base.Focus();
hiddenTextEntry.Focus();
}
private CodeView CreateItem(char? charValue = null)
{
CodeView container = new()
{
ItemFocusColor = ItemFocusColor,
FocusAnimationType = ItemFocusAnimation,
};
container.SetBinding(Border.HeightRequestProperty, new Binding("ItemSize", source: this));
container.SetBinding(Border.WidthRequestProperty, new Binding("ItemSize", source: this));
container.SetBinding(Border.StrokeThicknessProperty, new Binding("ItemBorderWidth", source: this));
container.Item.SetBinding(Border.BackgroundColorProperty, new Binding("ItemBackgroundColor", source: this));
container.CharLabel.FontSize = ItemSize / 2;
container.SecureMode(IsPassword);
container.SetColor(Color, ItemBorderColor);
container.SetRadius(ItemShape);
if (charValue.HasValue)
{
container.SetValueWithAnimation(charValue.Value);
}
return container;
}
#endregion Methods
#region Events
private void FreakyCodeView_TextChanged(object sender, TextChangedEventArgs e)
{
CodeValue = e.NewTextValue;
if (e.NewTextValue.Length >= CodeLength)
{
if (ShouldAutoDismissKeyboard)
{
hiddenTextEntry.DismissSoftKeyboard();
}
CodeEntryCompleted?.Invoke(this, new FreakyCodeCompletedEventArgs(CodeValue));
CodeEntryCompletedCommand.ExecuteCommandIfAvailable(CodeValue);
}
}
#endregion Events
#region BindableProperties
public string CodeValue
{
get => (string)GetValue(CodeValueProperty);
set => SetValue(CodeValueProperty, value);
}
public static readonly BindableProperty CodeValueProperty =
BindableProperty.Create(
nameof(CodeValue),
typeof(string),
typeof(FreakyCodeView),
string.Empty,
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: CodeValuePropertyChanged);
private static async void CodeValuePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
try
{
var control = (FreakyCodeView)bindable;
string newCode = newValue.ToString();
string oldCode = oldValue.ToString();
int newCodeLength = newCode.Length;
int oldCodeLength = oldCode.Length;
if (newCodeLength == 0 && oldCodeLength == 0)
{
return;
}
char[] newCodeChars = newCode.ToCharArray();
control.hiddenTextEntry.Text = newCode;
var CodeItemArray = control.CodeItemContainer.Children.Select(x => x as CodeView).ToArray();
bool isCodeEnteredProgramatically = (oldCodeLength == 0 && newCodeLength == control.CodeLength) || newCodeLength == oldCodeLength;
if (isCodeEnteredProgramatically)
{
for (int i = 0; i < control.CodeLength; i++)
{
CodeItemArray[i].ClearValueWithAnimation();
}
}
for (int i = 0; i < control.CodeLength; i++)
{
if (i < newCodeLength)
{
if (isCodeEnteredProgramatically)
{
await Task.Delay(50);
}
CodeItemArray[i].SetValueWithAnimation(newCodeChars[i]);
}
else
{
if (CodeItemArray.Length >= control.CodeLength)
{
CodeItemArray[i].ClearValueWithAnimation();
CodeItemArray[i].UnfocusAnimate();
}
}
}
if (control.hiddenTextEntry.IsFocused)
{
if (newCodeLength < control.CodeLength)
{
CodeItemArray[newCodeLength].FocusAnimate();
}
else if (newCodeLength == control.CodeLength)
{
CodeItemArray[newCodeLength - 1].FocusAnimate();
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
public int CodeLength
{
get => (int)GetValue(CodeLengthProperty);
set => SetValue(CodeLengthProperty, value);
}
public static readonly BindableProperty CodeLengthProperty =
BindableProperty.Create(
nameof(CodeLength),
typeof(int),
typeof(FreakyCodeView),
CodeView.DefaultCodeLength,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: CodeLengthPropertyChanged);
private static void CodeLengthPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if ((int)newValue <= 0)
{
return;
}
((FreakyCodeView)bindable).Initialize();
}
public KeyboardType CodeInputType
{
get => (KeyboardType)GetValue(CodeInputTypeProperty);
set => SetValue(CodeInputTypeProperty, value);
}
public static readonly BindableProperty CodeInputTypeProperty =
BindableProperty.Create(
nameof(CodeInputType),
typeof(KeyboardType),
typeof(FreakyCodeView),
KeyboardType.Numeric,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: CodeInputTypePropertyChanged);
private static void CodeInputTypePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = ((FreakyCodeView)bindable);
control.SetInputType((KeyboardType)newValue);
}
public void SetInputType(KeyboardType inputKeyboardType)
{
if (inputKeyboardType == KeyboardType.Numeric)
{
hiddenTextEntry.Keyboard = Keyboard.Numeric;
}
else if (inputKeyboardType == KeyboardType.AlphaNumeric)
{
hiddenTextEntry.Keyboard = Keyboard.Create(0);
}
}
public ICommand CodeEntryCompletedCommand
{
get { return (ICommand)GetValue(CodeEntryCompletedCommandProperty); }
set { SetValue(CodeEntryCompletedCommandProperty, value); }
}
public static readonly BindableProperty CodeEntryCompletedCommandProperty =
BindableProperty.Create(
nameof(CodeEntryCompletedCommand),
typeof(ICommand),
typeof(FreakyCodeView),
null);
public bool IsPassword
{
get => (bool)GetValue(IsPasswordProperty);
set => SetValue(IsPasswordProperty, value);
}
public static readonly BindableProperty IsPasswordProperty =
BindableProperty.Create(
nameof(IsPassword),
typeof(bool),
typeof(FreakyCodeView),
true,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: IsPasswordPropertyChanged);
private static void IsPasswordPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = ((FreakyCodeView)bindable);
foreach (var x in control.CodeItemContainer.Children)
{
var container = (CodeView)x;
container.SecureMode((bool)newValue);
}
}
public Color Color
{
get => (Color)GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
public static readonly BindableProperty ColorProperty =
BindableProperty.Create(
nameof(Color),
typeof(Color),
typeof(FreakyCodeView),
Colors.Black,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: ColorPropertyChanged);
private static void ColorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = ((FreakyCodeView)bindable);
foreach (var x in control.CodeItemContainer.Children)
{
var container = (CodeView)x;
container.SetColor(color: (Color)newValue, ItemBorderColor: control.ItemBorderColor);
}
}
public double ItemSpacing
{
get => (double)GetValue(ItemSpacingProperty);
set => SetValue(ItemSpacingProperty, value);
}
public static readonly BindableProperty ItemSpacingProperty =
BindableProperty.Create(
nameof(ItemSpacing),
typeof(double),
typeof(FreakyCodeView),
CodeView.DefaultItemSpacing);
public double ItemSize
{
get => (double)GetValue(ItemSizeProperty);
set => SetValue(ItemSizeProperty, value);
}
public static readonly BindableProperty ItemSizeProperty =
BindableProperty.Create(
nameof(ItemSize),
typeof(double),
typeof(FreakyCodeView),
CodeView.DefaultItemSize);
public ItemShape ItemShape
{
get => (ItemShape)GetValue(ItemShapeProperty);
set => SetValue(ItemShapeProperty, value);
}
public static readonly BindableProperty ItemShapeProperty =
BindableProperty.Create(
nameof(ItemShape),
typeof(ItemShape),
typeof(FreakyCodeView),
ItemShape.Circle,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: ItemShapePropertyChanged);
private static void ItemShapePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = ((FreakyCodeView)bindable);
foreach (var x in control.CodeItemContainer.Children)
{
var container = (CodeView)x;
container.SetRadius((ItemShape)newValue);
}
}
public Color ItemFocusColor
{
get => (Color)GetValue(ItemFocusColorProperty);
set => SetValue(ItemFocusColorProperty, value);
}
public static readonly BindableProperty ItemFocusColorProperty =
BindableProperty.Create(
nameof(ItemFocusColor),
typeof(Color),
typeof(FreakyCodeView),
Colors.Black,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: ItemFocusColorPropertyChanged);
private static void ItemFocusColorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = ((FreakyCodeView)bindable);
foreach (var x in control.CodeItemContainer.Children)
{
var container = (CodeView)x;
container.ItemFocusColor = (Color)newValue;
}
}
public FocusAnimation ItemFocusAnimation
{
get => (FocusAnimation)GetValue(ItemFocusAnimationProperty);
set => SetValue(ItemFocusAnimationProperty, value);
}
public static readonly BindableProperty ItemFocusAnimationProperty =
BindableProperty.Create(
nameof(ItemFocusAnimation),
typeof(FocusAnimation),
typeof(FreakyCodeView),
default(FocusAnimation),
defaultBindingMode: BindingMode.OneWay,
propertyChanged: ItemFocusAnimationPropertyChanged);
private static void ItemFocusAnimationPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = ((FreakyCodeView)bindable);
foreach (var x in control.CodeItemContainer.Children)
{
var container = (CodeView)x;
container.FocusAnimationType = (FocusAnimation)newValue;
}
}
public Color ItemBorderColor
{
get => (Color)GetValue(ItemBorderColorProperty);
set => SetValue(ItemBorderColorProperty, value);
}
public static readonly BindableProperty ItemBorderColorProperty =
BindableProperty.Create(
nameof(ItemBorderColor),
typeof(Color),
typeof(FreakyCodeView),
Colors.Black,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: ItemBorderColorPropertyChanged);
private static void ItemBorderColorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (FreakyCodeView)bindable;
if (control.Color != (Color)newValue)
{
foreach (var x in ((FreakyCodeView)bindable).CodeItemContainer.Children)
{
var container = (CodeView)x;
container.SetColor(color: control.Color, ItemBorderColor: (Color)newValue);
}
}
}
public Color ItemBackgroundColor
{
get => (Color)GetValue(ItemBackgroundColorProperty);
set => SetValue(ItemBackgroundColorProperty, value);
}
public static readonly BindableProperty ItemBackgroundColorProperty =
BindableProperty.Create(
nameof(ItemBackgroundColor),
typeof(Color),
typeof(FreakyCodeView),
default(Color),
defaultBindingMode: BindingMode.OneWay,
propertyChanged: ItemBackgroundColorPropertyChanged);
private static void ItemBackgroundColorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
foreach (var x in ((FreakyCodeView)bindable).CodeItemContainer.Children)
{
var container = (CodeView)x;
container.Item.BackgroundColor = (Color)newValue;
}
}
public bool ShouldAutoDismissKeyboard
{
get => (bool)GetValue(ShouldAutoDismissKeyboardProperty);
set => SetValue(ShouldAutoDismissKeyboardProperty, value);
}
public static readonly BindableProperty ShouldAutoDismissKeyboardProperty =
BindableProperty.Create(
nameof(ShouldAutoDismissKeyboard),
typeof(bool),
typeof(FreakyCodeView),
true);
private void TapGestureRecognizer_Tapped(object sender, TappedEventArgs e)
{
if (IsEnabled)
{
this.Focus();
}
}
public double ItemBorderWidth
{
get => (double)GetValue(ItemBorderWidthProperty);
set => SetValue(ItemBorderWidthProperty, value);
}
public static readonly BindableProperty ItemBorderWidthProperty =
BindableProperty.Create(
nameof(ItemBorderWidth),
typeof(double),
typeof(FreakyCodeView),
5.0,
defaultBindingMode: BindingMode.OneWay);
[TypeConverter(typeof(FontSizeConverter))]
public double FontSize
{
get => (double)GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
public static readonly BindableProperty FontSizeProperty =
BindableProperty.Create(
nameof(FontSize),
typeof(double),
typeof(FreakyCodeView),
defaultValueCreator: FontSizeDefaultValueCreator,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: OnFontSizeChanged);
private static object FontSizeDefaultValueCreator(BindableObject bindable)
{
var itemSize = (double)ItemSizeProperty.DefaultValue;
double fontSize = itemSize / 2.0;
return fontSize;
}
private static void OnFontSizeChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = ((FreakyCodeView)bindable);
foreach (var x in control.CodeItemContainer.Children)
{
var container = (CodeView)x;
container.CharLabel.FontSize = (double)newValue;
}
}
public string FontFamily
{
get => (string)GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(
nameof(FontFamily),
typeof(string),
typeof(FreakyCodeView),
propertyChanged: OnFontFamilyChanged);
private static void OnFontFamilyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = ((FreakyCodeView)bindable);
foreach (var x in control.CodeItemContainer.Children)
{
var container = (CodeView)x;
container.CharLabel.FontFamily = newValue?.ToString();
}
}
#endregion BindableProperties
}
Upvotes: 0