none
WPF: Как "выпустить" фокус из UserControl-a и передать его дальше? RRS feed

  • Вопрос

  • Добрый день!

    Столкнулся с непонятной проблемой. Создаю ListView, в нем переопределяю CellTemplate-ы для столбцов, один из столбцов выглядит так:

      <GridViewColumn Header="{x:Static r:Resources.ContactsEditor_Value}" Width="200">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <local:EditableListViewItem ValuePath="Value" IsAlwaysEditable="True" />
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
     </GridViewColumn>


    Там отображается и редактируется значение контакта пользователя - телефон, емейл, адрес и т.п. В зависимости от ситуации, EditableListViewItem подкидывает нужный темплейт поля ввода. В числе прочих может подкинуть контрол для ввода телефона: 

    Как видно, этот контрол (PhoneTextBox) состоит из ComboBox и TextBox. Так вот, при проходе клавиатурного фокуса от радиобаттона и дальше вправо, фокус "застревает" на контролах, лежащих внутри PhoneTextBox. То есть фокус начинает прыгать с комбобокса (префиксы номера) на текстбокс с самим номером и обратно. На комменты и куда-либо еще не уходит.

    Я сделал костыль внутри PhoneTextBox:

          private void ComboBox_OnKeyDown(object sender, KeyEventArgs e)
            {
                if (e.Key == Key.Tab)
                {
                    FocusedElement = _textBox;
                }
            }

            private void TextBox_OnKeyDown(object sender, KeyEventArgs e)
            {
                if (e.Key == Key.Tab)
                {
                    this.IsEnabled = false;
                    Application.Current.Dispatcher.BeginInvoke(new Action(async () =>
                    {
                        await Task.Delay(50);
                        this.IsEnabled = true;
                    }));
                }
            }

    Тогда фокус стал наконец уходить из временно выключенного контрола, но при приходе фокуса на ListViewItem первым получает фокус все равно почему-то не RadioButton, a PhoneTextBox (и тут я уже не знаю, какой можно костыль сделать).

    Почему так, подскажите, плиз?


Ответы

  • В итоге сделал так:

    PhoneTextBox.xaml.cs (FocusedElement - новое DependencyProperty):

            private bool _isNeedToGoToTextBox;
    
            private void PhoneTextBox_OnGotFocus(object sender, RoutedEventArgs e)
            {
                _isGotFocus = true;
                if (_isNeedToGoToTextBox)
                {
                    FocusedElement = _textBox;
                }
                else
                {
                    FocusedElement = comboBox;
                }
                _isNeedToGoToTextBox = false;
            }
    
            private void ComboBox_OnPreviewKeyDown(object sender, KeyEventArgs e)
            {
                if (e.Key == Key.Tab && Equals(FocusedElement, comboBox))
                {
                    FocusedElement = _textBox;
                    _isNeedToGoToTextBox = true;
                }
            }

    Соответственно, в XAML написано:

    <UserControl x:Class="WpfPanels.UserControls.PhoneTextBox"

    ...

    Name="phoneTextBox" GotFocus="PhoneTextBox_OnGotFocus"
            FocusManager.FocusedElement="{Binding FocusedElement, ElementName=phoneTextBox}">

    у ComboBox прописал событие PreviewKeyDown="ComboBox_OnPreviewKeyDown" 

    Для того, чтобы обойти по табу и стрелкам все элементы своего ListView, отказался вообще от стандартного обхода таба и написал свой.

    ContactsEditor.xaml.cs:

            public static readonly DependencyProperty ListGridViewColumnProperty =
                DependencyProperty.RegisterAttached("ListGridViewColumn", typeof(GridViewColumn), typeof(ContactsEditor), new PropertyMetadata(null));
    
            public static void SetListGridViewColumn(UIElement element, GridViewColumn value)
            {
                element.SetValue(ListGridViewColumnProperty, value);
            }
    
            public static GridViewColumn GetListGridViewColumn(UIElement element)
            {
                return (GridViewColumn)element.GetValue(ListGridViewColumnProperty);
            }
    //....
            
            private Point GetCoordsOfElement(FrameworkElement source)
            {
                var elem = source;
                while (GetListGridViewColumn(elem) == null && !(elem is ListViewItem))
                {
                    elem = ElementTreeHelper.GetParentOfType<FrameworkElement>(elem);
                }
                if (elem == null)
                {
                    throw new InvalidCastException($"Element {source} or its parent doesn't have ListGridViewColumn attribute");
                }
                return new Point
                {
                    Y = listView.Items.IndexOf(elem.DataContext),
                    X = ((GridView) listView.View).Columns.IndexOf(GetListGridViewColumn(elem))
                };
            }
    
            private FrameworkElement GetElemByCoords(Point coords)
            {
                var colCollection = ((GridView) listView.View).Columns;
                if (coords.X < 0 || coords.X >= colCollection.Count || coords.Y < 0 || coords.Y >= listView.Items.Count)
                {
                    return null;
                }
                var lvi = listView.ItemContainerGenerator.ContainerFromItem(listView.Items[(int) coords.Y]) as ListViewItem;
                var column = colCollection[(int) coords.X];
                var children = ElementTreeHelper.GetVisualChildrenList(lvi);
                var elem = children.FirstOrDefault(e => Equals(GetListGridViewColumn(e), column));
                return elem;
            }
    
            private void ContactsEditor_OnPreviewKeyDown(object sender, KeyEventArgs e)
            {
                var source = (FrameworkElement)e.OriginalSource;
                var coords = GetCoordsOfElement(source);
                if ((e.Key == Key.Down || e.Key == Key.Up) && !(source is ComboBox) 
                    || e.Key == Key.Left || e.Key == Key.Right || 
                    (e.Key == Key.Tab && ElementTreeHelper.GetParentOfType<PhoneTextBox>(source) == null))
                {
                    var direction = e.Key == Key.Up
                        ? FocusNavigationDirection.Up
                        : e.Key == Key.Down
                            ? FocusNavigationDirection.Down
                            : e.Key == Key.Left
                                ? FocusNavigationDirection.Left
                                : FocusNavigationDirection.Right; // Tab или Right - всё равно вправо
                    var colCollection = ((GridView)listView.View).Columns;
                    if (direction == FocusNavigationDirection.Right && (int)coords.X == colCollection.Count - 1)
                    {
                        coords.X = 0;
                        coords.Y += 1;
                    }
                    else if (direction == FocusNavigationDirection.Right)
                    {
                        coords.X += 1;
                    }
                    else if (direction == FocusNavigationDirection.Left)
                    {
                        coords.X -= 1;
                    }
                    else if (direction == FocusNavigationDirection.Up)
                    {
                        coords.Y -= 1;
                    }
                    else if (direction == FocusNavigationDirection.Down)
                    {
                        coords.Y += 1;
                    }
                    var x = GetElemByCoords(coords);
                    if (x != null)
                    {
                        x.Focus();
                        e.Handled = true;
                    }
                }
            }

    Столбцы в XAML описаны так:

     <GridViewColumn x:Name="columnValue" Header="{x:Static r:Resources.ContactsEditor_Value}"
                                Width="200">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <local:EditableListViewItem ValuePath="Value" IsAlwaysEditable="True" 
                                            local:ContactsEditor.ListGridViewColumn="{Binding ElementName=columnValue}" />
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>

    То есть к каждому элементу темплейта я привязал AttachedProperty ListGridViewColumn, по нему определяю координаты элемента при нажатии стрелок или таба, далее вычисляю, кто д.б. следующим и даю ему фокус.

    Здесь не показаны методы обхода визуального дерева для поиска родителей и наследников, если кому нужно - выложу.


    • Изменено SvarogichRed 29 мая 2018 г. 8:27
    • Помечено в качестве ответа SvarogichRed 29 мая 2018 г. 8:30

Все ответы

  • Здравствуйте,

    Быть может, стоит попробовать использовать: KeyboardNavigation.TabNavigation


    Если Вам помог чей-либо ответ, пожалуйста, не забывайте жать на кнопку "Предложить как ответ" или "Проголосовать за полезное сообщение" Мнения, высказанные здесь, являются отражение моих личных взглядов, а не позиции корпорации Microsoft. Вся информация предоставляется "как есть" без каких-либо гарантий.

    Модератор
  • В итоге сделал так:

    PhoneTextBox.xaml.cs (FocusedElement - новое DependencyProperty):

            private bool _isNeedToGoToTextBox;
    
            private void PhoneTextBox_OnGotFocus(object sender, RoutedEventArgs e)
            {
                _isGotFocus = true;
                if (_isNeedToGoToTextBox)
                {
                    FocusedElement = _textBox;
                }
                else
                {
                    FocusedElement = comboBox;
                }
                _isNeedToGoToTextBox = false;
            }
    
            private void ComboBox_OnPreviewKeyDown(object sender, KeyEventArgs e)
            {
                if (e.Key == Key.Tab && Equals(FocusedElement, comboBox))
                {
                    FocusedElement = _textBox;
                    _isNeedToGoToTextBox = true;
                }
            }

    Соответственно, в XAML написано:

    <UserControl x:Class="WpfPanels.UserControls.PhoneTextBox"

    ...

    Name="phoneTextBox" GotFocus="PhoneTextBox_OnGotFocus"
            FocusManager.FocusedElement="{Binding FocusedElement, ElementName=phoneTextBox}">

    у ComboBox прописал событие PreviewKeyDown="ComboBox_OnPreviewKeyDown" 

    Для того, чтобы обойти по табу и стрелкам все элементы своего ListView, отказался вообще от стандартного обхода таба и написал свой.

    ContactsEditor.xaml.cs:

            public static readonly DependencyProperty ListGridViewColumnProperty =
                DependencyProperty.RegisterAttached("ListGridViewColumn", typeof(GridViewColumn), typeof(ContactsEditor), new PropertyMetadata(null));
    
            public static void SetListGridViewColumn(UIElement element, GridViewColumn value)
            {
                element.SetValue(ListGridViewColumnProperty, value);
            }
    
            public static GridViewColumn GetListGridViewColumn(UIElement element)
            {
                return (GridViewColumn)element.GetValue(ListGridViewColumnProperty);
            }
    //....
            
            private Point GetCoordsOfElement(FrameworkElement source)
            {
                var elem = source;
                while (GetListGridViewColumn(elem) == null && !(elem is ListViewItem))
                {
                    elem = ElementTreeHelper.GetParentOfType<FrameworkElement>(elem);
                }
                if (elem == null)
                {
                    throw new InvalidCastException($"Element {source} or its parent doesn't have ListGridViewColumn attribute");
                }
                return new Point
                {
                    Y = listView.Items.IndexOf(elem.DataContext),
                    X = ((GridView) listView.View).Columns.IndexOf(GetListGridViewColumn(elem))
                };
            }
    
            private FrameworkElement GetElemByCoords(Point coords)
            {
                var colCollection = ((GridView) listView.View).Columns;
                if (coords.X < 0 || coords.X >= colCollection.Count || coords.Y < 0 || coords.Y >= listView.Items.Count)
                {
                    return null;
                }
                var lvi = listView.ItemContainerGenerator.ContainerFromItem(listView.Items[(int) coords.Y]) as ListViewItem;
                var column = colCollection[(int) coords.X];
                var children = ElementTreeHelper.GetVisualChildrenList(lvi);
                var elem = children.FirstOrDefault(e => Equals(GetListGridViewColumn(e), column));
                return elem;
            }
    
            private void ContactsEditor_OnPreviewKeyDown(object sender, KeyEventArgs e)
            {
                var source = (FrameworkElement)e.OriginalSource;
                var coords = GetCoordsOfElement(source);
                if ((e.Key == Key.Down || e.Key == Key.Up) && !(source is ComboBox) 
                    || e.Key == Key.Left || e.Key == Key.Right || 
                    (e.Key == Key.Tab && ElementTreeHelper.GetParentOfType<PhoneTextBox>(source) == null))
                {
                    var direction = e.Key == Key.Up
                        ? FocusNavigationDirection.Up
                        : e.Key == Key.Down
                            ? FocusNavigationDirection.Down
                            : e.Key == Key.Left
                                ? FocusNavigationDirection.Left
                                : FocusNavigationDirection.Right; // Tab или Right - всё равно вправо
                    var colCollection = ((GridView)listView.View).Columns;
                    if (direction == FocusNavigationDirection.Right && (int)coords.X == colCollection.Count - 1)
                    {
                        coords.X = 0;
                        coords.Y += 1;
                    }
                    else if (direction == FocusNavigationDirection.Right)
                    {
                        coords.X += 1;
                    }
                    else if (direction == FocusNavigationDirection.Left)
                    {
                        coords.X -= 1;
                    }
                    else if (direction == FocusNavigationDirection.Up)
                    {
                        coords.Y -= 1;
                    }
                    else if (direction == FocusNavigationDirection.Down)
                    {
                        coords.Y += 1;
                    }
                    var x = GetElemByCoords(coords);
                    if (x != null)
                    {
                        x.Focus();
                        e.Handled = true;
                    }
                }
            }

    Столбцы в XAML описаны так:

     <GridViewColumn x:Name="columnValue" Header="{x:Static r:Resources.ContactsEditor_Value}"
                                Width="200">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <local:EditableListViewItem ValuePath="Value" IsAlwaysEditable="True" 
                                            local:ContactsEditor.ListGridViewColumn="{Binding ElementName=columnValue}" />
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>

    То есть к каждому элементу темплейта я привязал AttachedProperty ListGridViewColumn, по нему определяю координаты элемента при нажатии стрелок или таба, далее вычисляю, кто д.б. следующим и даю ему фокус.

    Здесь не показаны методы обхода визуального дерева для поиска родителей и наследников, если кому нужно - выложу.


    • Изменено SvarogichRed 29 мая 2018 г. 8:27
    • Помечено в качестве ответа SvarogichRed 29 мая 2018 г. 8:30
  • Сейчас уже сделал, как-то работает, в следующий раз попробую. Спасибо :)