I have created a custom behavior for the Entry control that allows a collection of Entry controls to provide a single-digit code input field that moves the cursor to the next field ( to the right ) when a value is entered, and to the previous field ( to the left ) when a value is deleted. This mostly works. There are still some bugs to work out in the actual behavior. The issue I am trying to resolve right now is that the string array that is the backing field in the ViewModel is not getting set calls, which means I can't raise property change notifications.
The custom behavior code is listed below..
public class AutoFocusBehavior : Behavior<Entry>
{
private string previousText = string.Empty;
public static readonly BindableProperty EntryTextChangedProperty =
BindableProperty.Create(nameof(EntryTextChanged),
typeof(EventHandler<TextChangedEventArgs>),
typeof(AutoFocusBehavior),
null);
public event EventHandler<TextChangedEventArgs> EntryTextChanged;
protected override void OnAttachedTo(Entry entry)
{
base.OnAttachedTo(entry);
entry.Focused += OnEntryFocused;
entry.TextChanged += OnEntryTextChanged;
}
protected override void OnDetachingFrom(Entry entry)
{
base.OnDetachingFrom(entry);
entry.Focused -= OnEntryFocused;
entry.TextChanged -= OnEntryTextChanged;
}
private void OnEntryFocused(object sender, FocusEventArgs e)
{
if (sender is Entry entry)
{
// Store the previous text if it's not null or empty
if (!string.IsNullOrEmpty(entry.Text))
{
previousText = entry.Text;
}
// Select all text when the Entry is focused
entry.SelectionLength = entry.Text?.Length ?? 0;
}
}
private void OnEntryTextChanged(object sender, TextChangedEventArgs e)
{
if (sender is Entry entry)
{
if (e.NewTextValue.Length == 0 && e.OldTextValue.Length == 1)
{
// If the user deletes a character, move focus to the previous entry
if (!MoveFocusToPreviousEntry(entry))
{
// If it's the first entry, do not move focus
entry.Text = string.Empty;
}
}
else if (e.NewTextValue.Length == 1)
{
// If the text length is 1, move focus to the next entry and clear the current entry
entry.Unfocus();
MoveFocusToNextEntry(entry);
}
EntryTextChanged?.Invoke(sender, e);
}
}
private void MoveFocusToNextEntry(Entry currentEntry)
{
// Find the parent stack layout containing all the Entry elements
if (currentEntry.Parent is StackLayout stackLayout)
{
// Get the index of the current Entry
int currentIndex = stackLayout.Children.IndexOf(currentEntry);
// Move focus to the next Entry if it exists
if (currentIndex < stackLayout.Children.Count - 1)
{
Entry nextEntry = (Entry)stackLayout.Children[currentIndex + 1];
nextEntry.Focus();
}
}
}
private bool MoveFocusToPreviousEntry(Entry currentEntry)
{
// Find the parent stack layout containing all the Entry elements
if (currentEntry.Parent is StackLayout stackLayout)
{
// Get the index of the current Entry
int currentIndex = stackLayout.Children.IndexOf(currentEntry);
// If it's the first Entry and the text is empty, do not move focus
if (currentIndex == 0 && string.IsNullOrEmpty(currentEntry.Text))
{
return false;
}
// Move focus to the previous Entry if it exists
if (currentIndex > 0)
{
Entry previousEntry = (Entry)stackLayout.Children[currentIndex - 1];
previousEntry.Focus();
return true;
}
}
return false;
}
}
The XAML that this code is bound to is
<StackLayout x:Name="EntryStack" Orientation="Horizontal" HorizontalOptions="CenterAndExpand" Spacing="1" Grid.Row="1" Margin="0, 10">
<!-- Code Entry Boxes -->
<Entry WidthRequest="40" Text="{Binding CodeDigits[0], Mode=TwoWay}" Keyboard="Numeric" MaxLength="1" HorizontalTextAlignment="Center">
<Entry.Behaviors>
<b:AutoFocusBehavior EntryTextChanged="{Binding EntryTextChangedCommand}" />
</Entry.Behaviors>
</Entry>
<Entry WidthRequest="40" Text="{Binding CodeDigits[1], Mode=TwoWay}" Keyboard="Numeric" MaxLength="1" HorizontalTextAlignment="Center">
<Entry.Behaviors>
<b:AutoFocusBehavior EntryTextChanged="{Binding EntryTextChangedCommand}" />
</Entry.Behaviors>
</Entry>
<Entry WidthRequest="40" Text="{Binding CodeDigits[2], Mode=TwoWay}" Keyboard="Numeric" MaxLength="1" HorizontalTextAlignment="Center">
<Entry.Behaviors>
<b:AutoFocusBehavior EntryTextChanged="{Binding EntryTextChangedCommand}" />
</Entry.Behaviors>
</Entry>
<Entry WidthRequest="40" Text="{Binding CodeDigits[3], Mode=TwoWay}" Keyboard="Numeric" MaxLength="1" HorizontalTextAlignment="Center">
<Entry.Behaviors>
<b:AutoFocusBehavior EntryTextChanged="{Binding EntryTextChangedCommand}" />
</Entry.Behaviors>
</Entry>
<Entry WidthRequest="40" Text="{Binding CodeDigits[4], Mode=TwoWay}" Keyboard="Numeric" MaxLength="1" HorizontalTextAlignment="Center">
<Entry.Behaviors>
<b:AutoFocusBehavior EntryTextChanged="{Binding EntryTextChangedCommand}" />
</Entry.Behaviors>
</Entry>
<Entry WidthRequest="40" Text="{Binding CodeDigits[5], Mode=TwoWay}" Keyboard="Numeric" MaxLength="1" HorizontalTextAlignment="Center">
<Entry.Behaviors>
<b:AutoFocusBehavior EntryTextChanged="{Binding EntryTextChangedCommand}" />
</Entry.Behaviors>
</Entry>
<Entry WidthRequest="40" Text="{Binding CodeDigits[6], Mode=TwoWay}" Keyboard="Numeric" MaxLength="1" HorizontalTextAlignment="Center">
<Entry.Behaviors>
<b:AutoFocusBehavior EntryTextChanged="{Binding EntryTextChangedCommand}" />
</Entry.Behaviors>
</Entry>
<Entry WidthRequest="40" Text="{Binding CodeDigits[7], Mode=TwoWay}" Keyboard="Numeric" MaxLength="1" HorizontalTextAlignment="Center">
<Entry.Behaviors>
<b:AutoFocusBehavior EntryTextChanged="{Binding EntryTextChangedCommand}" />
</Entry.Behaviors>
</Entry>
</StackLayout>
The Property the view model is binding to is ...
public string[] CodeDigits
{
get { return _codeDigits; }
set
{
if (_codeDigits != value)
{
_codeDigits = value;
RaisePropertyChanged(nameof(CodeDigits));
RaisePropertyChanged(nameof(IsVerifyButtonEnabled));
}
}
}
I have tried a number of different things to get the setter fired, including adding the BindableProperty for the behavior and manually firing the EntryTextChanged event from the behaviors own OnEntryTextChanged handler. I am missing something fundamental here. Any help/guidance is appreciated :-)
Your setter is not called because it belongs to
CodeDigitsproperty, not to the strings contained in it.You may try using
ObservableCollectioninstead ofstring[], but I'm not sure it would change something.I see two ways.
If you know how many entries you need, let's say 8, you can use 8
stringproperties and call RaisePropertyChanged for each of them.Otherwise, you have to create a more complex object that implement
INotifyPropertyChangedand has astringproperty and then you store 8 of these objects in the array.