Как по нажатию кнопки Enter переключать фокус на следующий элемент в Avalonia MVVM
У меня есть динамические TextBox, они добавляются по нажатию кнопки. Мне необходимо по нажатию кнопки Enter внутри TextBox переключать фокус на следующий элемент, а если элемента не существует создавать его. Сейчас у меня при создании элемента фокус устанавливается на него.
View:
<Grid RowDefinitions="40,*">
<Button Grid.Row="0" Content="New row" Command="{Binding AddNewRow}" />
<ItemsControl Grid.Row="1" ItemsSource="{Binding ElementsList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding Id}" Width="20" VerticalAlignment="Center"/>
<TextBox Text="{Binding Text}" Loaded="Control_OnLoaded"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Аналогия с двумя tb:
<Grid RowDefinitions="40,*">
<Button Grid.Row="0" Content="New row" Command="{Binding AddNewRow}" />
<ItemsControl Grid.Row="1" ItemsSource="{Binding ElementsList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding Id}" Width="20" VerticalAlignment="Center"/>
<TextBox Text="{Binding Text}" Loaded="Control_OnLoaded"/>
<TextBox Text="{Binding Text}" Loaded="Control_OnLoaded"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
```
View.CS:
private void Control_OnLoaded(object? sender, RoutedEventArgs e)
{
var tb = sender as TextBox;
tb.Focus();
}
models:
public class Elements: ViewModelBase
{
private int _id;
private string _text;
public int Id
{
get => _id;
set => this.RaiseAndSetIfChanged(ref _id, value);
}
public string Text
{
get => _text;
set => this.RaiseAndSetIfChanged(ref _text, value);
}
}
VM:
public class MainWindowViewModel : ViewModelBase
{
public ReactiveCommand<Unit, Unit> AddNewRow { get; set; }
private ObservableCollection<Elements>? _elementsList;
public ObservableCollection<Elements>? ElementsList
{
get => _elementsList;
set => this.RaiseAndSetIfChanged(ref _elementsList, value);
}
public MainWindowViewModel()
{
ElementsList = new ObservableCollection<Elements>(new List<Elements>());
ElementsList.Add(new ObservableCollection<Elements>
{
new()
{
Id = ElementsList.Count + 1, Text = ""
}
});
AddNewRow = ReactiveCommand.Create(() =>
{
ElementsList.Add(new ObservableCollection<Elements>
{
new()
{
Id = ElementsList.Count + 1, Text = ""
}
});
});
}
Ответы (3 шт):
UPD: нашёл более простое решение https://ru.stackoverflow.com/a/1557250/248343
Как заставить добавлять новые TextBox по нажатию Enter:
Простейшим способом будет установить кнопке атрибут IsDefault = true:
<Button Grid.Row="0" Content="New row" Command="{Binding AddNewRow}"
IsDefault="True"/>
В этом случае команда будет срабатывать всегда, даже если в фокусе будет не TextBox.
Чтобы этого избежать можно добавить к TextBox обработчик события нажатия клавиши, например KeyDown:
<TextBox Text="{Binding Text}"
Loaded="Control_OnLoaded"
KeyDown="Control_KeyDown"/>
axaml.cs:
private void Control_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
var dc = DataContext as MainWindowViewModel;
dc.AddNewRow.Execute();
}
}
Как менять фокус на следующий элемент, если не надо добавлять новый:
Вот тут нормального решения не нашёл. Как по мне так не слой VM должен заниматься переключением фокуса на View. Но как сейчас в Авалонии правильно работать с фокусом не знаю.
А если через VM, то
- В
Elementsдобавляем свойствоFocused - Изменяем команду
AddNewRowтак, чтобы она принимала параметр типаElements
public ReactiveCommand<Elements, Unit> AddNewRow { get; set; }
...
AddNewRow = ReactiveCommand.Create<Elements>((el) =>
{
if (el == ElementsList.Last())
{
ElementsList.Where(x => x.Focused).Single().Focused = false;
ElementsList.Add(
new()
{
Id = ElementsList.Count + 1,
Text = "",
Focused = true
}
);
}
else
{
ElementsList.Where(x => x.Focused).Single().Focused = false;
var index = ElementsList.IndexOf(el);
ElementsList[index + 1].Focused = true;
}
});
- Подписываемся на событие изменения свойства
Focused
private void Control_OnLoaded(object? sender, RoutedEventArgs e)
{
var tb = sender as TextBox;
var dc = tb.DataContext as Elements;
dc.PropertyChanged += (s, e) =>
{
if (e.PropertyName == "Focused")
{
if (dc.Focused)
tb.Focus();
}
};
tb.Focus(); //установка фокуса для нового элемента
}
- Изменяем вызов команды при нажатии
EnterвTextBox, чтобы передать какой конкретноTextBoxбыл нажат
private void Control_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
var tb = sender as TextBox;
var dc = DataContext as MainViewModel;
dc.AddNewRow.Execute(tb.DataContext as Elements).Subscribe();
}
}
Нашёл лучшее решение, в котором для переключения фокуса не используется слой VM, и которое работает с двумя TextBox-ами.
Для обработки нажатия клавиши Enter используем событие KeyDown. Плюс помечаем интересующие нас TextBox-ы, чтобы при переключении фокуса не переключиться на другие элементы, находящиеся на этой же форме (Используем для этого аттрибут Tag):
<TextBox Text="{Binding Text}" Loaded="Control_OnLoaded"
KeyDown="Control_KeyDown"
Tag="First"/>
<TextBox Text="{Binding Text1}" Loaded="Control_OnLoaded"
KeyDown="Control_KeyDown"
Tag="Second"/>
Если нужно переключаться змейкой:
Code-behind будет выглядеть следующим образом:
private void Control_OnLoaded(object? sender, RoutedEventArgs e)
{
var tb = sender as TextBox;
// поскольку разом добавляется два TextBox-а, то фокус нужно поставить на первый из них
if (tb.Tag == "First")
{
tb.Focus();
}
}
private void Control_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
var tb = sender as TextBox;
if (IsLastTextBox(tb)) // если Enter нажат в последнем TextBox-е, то нужно создать новую строку
{
CreateNewRow(tb);
}
else // иначе фокус должен переключиться на уже существующий TextBox
{
SwitchFocusToNextRow(tb);
}
}
}
private bool IsLastTextBox(TextBox tb)
{
var nextElement = KeyboardNavigationHandler.GetNext(tb, NavigationDirection.Next);
if (IsOurTextbox(nextElement)) // если tb - последний созданный TextBox, то за ним будет следовать не "наш" элемент
{
return false;
}
return true;
}
// "нашим" является TextBox, помеченный либо First, либо Second
private bool IsOurTextbox(IInputElement element)
{
if (element is TextBox nextTb)
{
if (nextTb.Tag == "First" || nextTb.Tag == "Second")
{
return true;
}
}
return false;
}
private void SwitchFocusToNextRow(TextBox tb)
{
var nextElement = KeyboardNavigationHandler.GetNext(tb, NavigationDirection.Next);
nextElement.Focus();
}
private void CreateNewRow(TextBox? tb)
{
var dc = DataContext as MainViewModel;
dc.AddNewRow.Execute(tb.DataContext as Elements).Subscribe();
}
Если нужно переключаться независимо в каждом из столбцов:
Code-behind будет выглядеть следующим образом:
TextBox previousFocused; // предыдущий элемент с фокусом
private void Control_OnLoaded(object? sender, RoutedEventArgs e)
{
var tb = sender as TextBox;
// поскольку разом добавляется два TextBox-а, то фокус нужно поставить не на тот,
// который был последним, а на тот, которых находится в столбце,
// в котором был нажат Enter
if (previousFocused == null || tb.Tag == previousFocused.Tag)
{
tb.Focus();
previousFocused = tb;
}
// нужно при переключении фокуса на уже созданный ранее элемент
tb.GotFocus += Tb_GotFocus;
}
private void Tb_GotFocus(object? sender, GotFocusEventArgs e)
{
previousFocused = sender as TextBox;
}
private void Control_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
var tb = sender as TextBox;
// если Enter нажат в последнем ряду, то нужно создать новую строку
if (InTheLastRow(tb))
{
CreateNewRow(tb);
}
else // иначе фокус должен переключиться на уже существующий TextBox
{
SwitchFocusToNextRow(tb);
}
}
}
private bool InTheLastRow(TextBox tb)
{
var nextElement = KeyboardNavigationHandler.GetNext(tb, NavigationDirection.Next);
// если tb - последний созданный TextBox, то за ним будет следовать не "наш" элемент
if (IsOurTextbox(nextElement))
{
// если tb - предпоследний созданный TextBox,
// то за ним будет следовать не "наш" элемент
nextElement = KeyboardNavigationHandler.GetNext(nextElement,
NavigationDirection.Next);
if (IsOurTextbox(nextElement))
{
// если два следующих элемента "наши", то мы не в последней строке
return false;
}
}
return true;
}
// "нашим" является TextBox, помеченный либо First, либо Second
private bool IsOurTextbox(IInputElement element)
{
if (element is TextBox nextTb)
{
if (nextTb.Tag == "First" || nextTb.Tag == "Second")
{
return true;
}
}
return false;
}
private void SwitchFocusToNextRow(TextBox tb)
{
var nextElement = KeyboardNavigationHandler.GetNext(tb, NavigationDirection.Next);
nextElement = KeyboardNavigationHandler.GetNext(nextElement, NavigationDirection.Next);
nextElement.Focus();
previousFocused = nextElement as TextBox;
}
private void CreateNewRow(TextBox? tb)
{
var dc = DataContext as MainViewModel;
dc.AddNewRow.Execute(tb.DataContext as Elements).Subscribe();
}
Таким образом, теги нам нужны при проверке, что мы не вышли за границы создаваемой таблицы, и для того, чтобы правильно назначить фокус при добавлении новой строки
При нажатии enter вызывается next(1); ну или если находитесь в группе радиокнопки то при нажатии стрелок вверх и вниз .Вероятно недоработано для DatePicker и может еще чего. В работе не тестировано ,но вдруг поможет .
private async void next(int napr, int keys = 0)
{
string? gruname = "";
IInputElement? a = FocusManager?.GetFocusedElement();
if (a?.GetType() == typeof(RadioButton))
{ RadioButton rr = (RadioButton)a; if (!string.IsNullOrEmpty(rr.GroupName)) { gruname = rr.GroupName; } }
if (napr == 1)
await Dispatcher.UIThread.InvokeAsync(() => a = KeyboardNavigationHandler.GetNext(a, NavigationDirection.Next));
else
await Dispatcher.UIThread.InvokeAsync(() => a = KeyboardNavigationHandler.GetNext(a, NavigationDirection.Previous));
await Dispatcher.UIThread.InvokeAsync(() => a?.Focus());
if (a?.GetType() == typeof(RadioButton))
{
RadioButton r = (RadioButton)a;
if (!string.IsNullOrEmpty(r.GroupName) && r.IsChecked == false)
{
while (0 == 0)
{
if (napr == 1)
await Dispatcher.UIThread.InvokeAsync(() => a = KeyboardNavigationHandler.GetNext(a, NavigationDirection.Next));
else
await Dispatcher.UIThread.InvokeAsync(() => a = KeyboardNavigationHandler.GetNext(a, NavigationDirection.Previous));
await Dispatcher.UIThread.InvokeAsync(() => a?.Focus());
if (a?.GetType() != typeof(RadioButton))
break;
RadioButton r1 = (RadioButton)a;
if (!string.IsNullOrEmpty(gruname) && gruname != r1.GroupName)
{
if (napr == 1)
await Dispatcher.UIThread.InvokeAsync(() => a = KeyboardNavigationHandler.GetNext(a, NavigationDirection.Previous));
else
await Dispatcher.UIThread.InvokeAsync(() => a = KeyboardNavigationHandler.GetNext(a, NavigationDirection.Next));
await Dispatcher.UIThread.InvokeAsync(() => a?.Focus());
return;
}
if (keys > 0) { break; };
if (r1.IsChecked == true && keys == 0) { r = r1; break; }
}
}
r.Focus();
}
else if (a?.GetType() == typeof(TextBox))
{
TextBox t = (TextBox)a;
inputs = 0;
t.SelectAll();
}
else if (a?.GetType() == typeof(CheckBox))
{
CheckBox t = (CheckBox)a;
t.BorderBrush = Avalonia.Media.Brush.Parse("Black");
t.BorderThickness = Avalonia.Thickness.Parse("0.5");
}
}