ListView 弹出键功能

ListView Popup key functionality

我实现了一个显示为弹出列表的列表视图。现在我想向它添加关键功能,比如每当在文本框中按下向上箭头时,它应该 select 我的列表视图中的一个项目,如果继续按下 KEY_UP/DOWN 它应该继续改变其索引分别。

这是EditMessageTextBox和关联的EditMessageTagPopup

这是使用的 XAML 代码:

<Grid x:Name="EditGrid"
      Grid.Row="1"
      Visibility="{Binding EditMessageControlVisibility}"
      FocusManager.IsFocusScope="False"
      VerticalAlignment="Center"
      Grid.Column="1"
      HorizontalAlignment="Stretch">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
  </Grid.RowDefinitions>
  <Border x:Name="EditMessageBorder"
          Grid.Row="0"
          BorderThickness="1"
          CornerRadius="1"
          Margin="0,10,0,0"
          BorderBrush="Gray">
    <Grid>
      <TextBlock FontSize="16"
                 Margin="10,0,0,3"
                 VerticalAlignment="Center"
                 HorizontalAlignment="Left"
                 Text="Edit message"
                 Foreground="{StaticResource brushWatermarkForeground}"
                 Visibility="{Binding ElementName=EditMessageTextBox, Path=Text.IsEmpty, Converter={StaticResource BooleanToVisibilityConverter}}" />
      <TextBox Name="EditMessageTextBox"
               Text="{Binding MessageToEdit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
               BorderBrush="Transparent"
               BorderThickness="0"
               Foreground="Black"
               FontSize="16"
               Margin="8,1,1,1"
               VerticalContentAlignment="Center"
               HorizontalContentAlignment="Left"
               MinHeight="35"
               ScrollViewer.VerticalScrollBarVisibility="Auto"
               TextWrapping="Wrap"
               AcceptsReturn="False"
               KeyUp="OnEditMessage_KeyUp"
               SpellCheck.IsEnabled="true" />
    </Grid>
  </Border>
  <StackPanel Grid.Row="1"
              Margin="0,10"
              Orientation="Horizontal">
    <Button Background="Transparent"
            VerticalContentAlignment="Center"
            Padding="5,2,5,3"
            Foreground="Black"
            BorderBrush="Gray"
            BorderThickness="0.8"
            Width="100"
            materialDesign:ShadowAssist.ShadowDepth="Depth0"
            Click="EditMessageCancelButton_Clicked">Cancel</Button>
    <Button Name="EditMessageButton"
            VerticalContentAlignment="Center"
            Padding="5,2,5,3"
            Background="#007a5a"
            Foreground="White"
            BorderBrush="#007a5a"
            Margin="15,0,0,0"
            materialDesign:ShadowAssist.ShadowDepth="Depth0"
            BorderThickness="0.8"
            IsEnabled="True"
            Width="140"
            Content="Save Changes"
            Click="EditMessageSaveButton_Clicked" />
  </StackPanel>    

  <Popup x:Name="EditMessageTagPopup"
         AllowsTransparency="True"
         IsOpen="{Binding IsOpenTagPopUp}"
         StaysOpen="False"
         Placement="Top"
         PlacementTarget="{Binding ElementName=EditMessageTextBox}">
    <Border materialDesign:ShadowAssist.ShadowDepth="Depth5"
            CornerRadius="5"
            Background="White"
            BorderBrush="Black"
            BorderThickness="0.8"
            MaxHeight="200">
      <ListView x:Name="EditTaggedUsers"
                Focusable="True"
                IsSynchronizedWithCurrentItem="True"
                ItemsSource="{Binding Source={StaticResource UserListForTag}}"
                SelectionChanged="EditMessageTagList_SelectionChanged">
        <ListView.ItemContainerStyle>
          <Style TargetType="ListViewItem">
            <Setter Property="Template">
              <Setter.Value>
                <ControlTemplate TargetType="ListViewItem">
                  <Border Name="_Border"
                          Padding="8">
                    <ContentPresenter />
                  </Border>
                  <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver"
                             Value="True">
                      <Setter TargetName="_Border"
                              Property="Background"
                              Value="#FF3BD38E" />
                      <Setter Property="Foreground"
                              Value="White" />
                    </Trigger>
                    <Trigger Property="IsSelected"
                             Value="True">
                      <Setter TargetName="_Border"
                              Property="Background"
                              Value="#FF205B4B" />
                      <Setter Property="Foreground"
                              Value="White" />
                    </Trigger>
                  </ControlTemplate.Triggers>
                </ControlTemplate>
              </Setter.Value>
            </Setter>
          </Style>
        </ListView.ItemContainerStyle>
        <ListView.ItemTemplate>
          <DataTemplate>
            <Grid Margin="-15,0,0,0"
                  Width="500">
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="*" />
              </Grid.ColumnDefinitions>
              <Rectangle Grid.Column="0"
                         RadiusY="5"
                         RadiusX="5"
                         Height="20"
                         Width="20">
                <Rectangle.Fill>
                  <ImageBrush ImageSource="{Binding ProfileImage}"
                              Stretch="UniformToFill" />
                </Rectangle.Fill>
              </Rectangle>
              <TextBlock Grid.Column="1"
                         Text="{Binding FullName}"
                         Margin="-10,0,0,0" />
            </Grid>
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
    </Border>
  </Popup>
</Grid>

下面是代码:

 ApplicationContext.StoredEditingMessage = (String)ApplicationContext.EditMessageText;
                var messageData = ((TextBox)sender).DataContext as ChatsModel;
                var EditMessagePopup = FindEditMessagePopup(MessageList);
                Border EditEessageBorder = EditMessagePopup.Child as Border;
                ListView EditMessageTagList = EditEessageBorder.Child as ListView;

                Dispatcher?.Invoke(() =>
                {
                    if (_contactsViewModel.GroupedChatByDate
                        .Find(x => messageData != null && x.MessageGuid == messageData.MessageGuid)
                        .IsOpenTagPopUp == false) return;
                    var index = _contactsViewModel.UsersListForTag.IndexOf(_contactsViewModel.UsersListForTag.FirstOrDefault(x => x.Selected == true));


                    switch (e.Key)
                    {
                        case Key.Up:
                            if (EditMessageTagList.SelectedIndex > 0)
                            {
                                EditMessageTagList.SelectedIndex -= 1;
                                EditMessageTagList.ScrollIntoView(EditMessageTagList.Items[EditMessageTagList.SelectedIndex]);
                            }
                            else
                            {
                                EditMessageTagList.SelectedIndex = _contactsViewModel.UsersListForTag.Count - 1;
                                EditMessageTagList.ScrollIntoView(EditMessageTagList.Items[EditMessageTagList.SelectedIndex]);
                            }
                            break;
                        case Key.Down:
                            if (EditMessageTagList.SelectedIndex + 1 == _contactsViewModel.UsersListForTag.Count)
                            {
                                EditMessageTagList.SelectedIndex = 0;
                                _contactsViewModel.UsersListForTag[index].Selected = true;
                                EditMessageTagList.ScrollIntoView(EditMessageTagList.Items[EditMessageTagList.SelectedIndex]);
                            }
                            else
                            {
                                EditMessageTagList.SelectedIndex += 1;
                                _contactsViewModel.UsersListForTag[index].Selected = true;
                                EditMessageTagList.ScrollIntoView(EditMessageTagList.Items[EditMessageTagList.SelectedIndex]);
                            }
                            break;
                    }
                    _contactsViewModel.UsersListForTag.ForEach(x => x.Selected = false);
                    if (index != -1)
                    {
                        _contactsViewModel.UsersListForTag[index].Selected = true;
                    }
                });

我尝试在滚动到 view() 中添加一个项目而不是 selected 索引但没有更新

当有一个完美的 selection made 时,这个函数从代码后面调用

private void EditMessageTagList_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        try
        {
            var messageModel = ((ListView)sender).DataContext as ChatsModel;

            if (((ListView)sender).SelectedItem is UserModel selectedUserForTag)
            {
               // _contactsViewModel.GroupedChatByDate.Find(x => messageModel != null && x.MessageGuid == messageModel.MessageGuid) .IsOpenTagPopUp = false;
                string SelectedTag = (selectedUserForTag.Id == ApplicationContext.CurrentLoggedInUserGuid) ? $"{selectedUserForTag.UserName.Replace("(you) ", "")} " : $"{selectedUserForTag.UserName} ";
                _contactsViewModel.GroupedChatByDate.Find
                    (x => messageModel != null && x.MessageGuid == messageModel.MessageGuid)
                .MessageToEdit = "@" + SelectedTag;
            }

            // ((ListView) sender).SelectedItem = null;
        }
        catch (Exception exception)
        {
            LoggingManager.Error(exception);
        }
    }

Here is screen recording regarding issue

Here is working functionality

问题在于,每次导航到 ListView 的下一项后,您都希望将焦点设置到绑定到 SelectedItem 的选择 TextBox。否则,在箭头键的帮助下导航 ListView 的项目已经是 ListView 的默认行为。

最简单的解决方案是在选择 TextBox 上使用 UIElement.InputBinding 捕获键盘输入(这可以处理视图模型中的键),然后

  1. Select next/previous 项
  2. 将 SelectedItem 滚动到视图中
  3. 将焦点移至所选内容TextBox
  4. 将选择的插入符号[=​​15=]移到末尾

DataItem.cs

class DataItem
{
  public string FullName { get; set; }

  public DataItem(string fullName) => this FullName = fullName;
}

ViewModel.cs

class ViewModel : INotifyPropertyChanged
{
  public ObservableCollection<DataItem> DataItems { get; set; }
  public ICommand SelectNextCommand => new AsyncRelayCommand(SelectNextItem);
  public ICommand SelectPreviousCommand => new AsyncRelayCommand(SelectPreviousItem);  

  private bool IsSelectedItemChangeInternal { get; set; }

  private DataItem selectedDataItem;
  public DataItem SelectedDataItem 
  {
    get => this.selectedDataItem; 
    set
    {
      this.selectedDataItem = value;
      OnPropertyChanged();

      // Do not filter the list when the selected item was set by the user
      // e.g. by using arrow keys
      if (!this.IsSelectedItemChangeInternal)
      {
        UpdateSearchFilter();
      }
    }
  }         

  private string filterKey;
  public string FilterKey
  {
    get => this.filterKey; 
    set
    {
      this.filterKey = value;
      OnPropertyChanged();

      // Only apply filters when the FilterKey was changed by the user
      // e.g. by editing the edit TextBox that binds to this property
      if (!this.IsSelectedItemChangeInternal)
      {
        ApplySearchFilter();
      }
    }        
  }         

  public ViewModel()
  {
    this.DataItems = new ObservableCollection<DataItems>();
    for (var index = 0; index < 100; index++)
    {
      this.DataItems.Add(new DataItem("name " + index.ToString());
    }
  }      

  private void ApplySearchFilter()
  {
    ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.Games);
    this.IsSelectedItemChangeInternal = true;

    collectionView.Filter = item => 
      string.IsNullOrWhiteSpace(this.FilterKey) || (item as DetailItem).FullName.StartsWith(this.FilterKey);

    // pre-select the first match
    collectionView.MoveCurrentToFirst();

    this.IsSelectedItemChangeInternal = false;
  }  

  private void UpdateSearchFilter()
  {        
    this.IsSelectedItemChangeInternal = true;
    this.FilterKey = this.SelectedDataItem.FullName;
    this.IsSelectedItemChangeInternal = false;
  }

  private void SelectNextItem()
  {
    ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.DataItems);
    collectionView.MoveCurrentToNext();

    // Loop
    if (collectionView.IsCurrentAfterLast)
    {
      collectionView.MoveCurrentToFirst();
    }
  }

  private void SelectPreviousItem()
  {
    ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.DataItems);
    collectionView.MoveCurrentToPrevious();

    // Loop
    if (collectionView.IsCurrentBeforeFirst)
    {
      collectionView.MoveCurrentToLast();
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;
  private void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName);
  }
}

MainWindow.xaml.cs

private void AdjustFocus_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
  var listView = sender as ListView;
  listView.ScrollIntoView(listView.SelectedItem);

  Application.Current.Dispatcher.InvokeAsync(() =>
  {
    Keyboard.Focus(this.EditMessageTextBox);
    this.EditMessageTextBox.CaretIndex = this.EditMessageTextBox.Text.Length;
  });
}

private void AdjustFocus_OnOpened(object sender, EventArgs e)
{
  this.EditTaggedUsers.Focus();
}

MainWindow.xaml

<Window>
  <Window.DataContext>
    <ViewModel />
  </Window.DataContex>

  <Grid>
    <TextBox x:Name="EditMessageTextBox"
             Text="{Binding FilterKey}">
      <TextBox.InputBindings>
        <KeyBinding Key="Down"
                    Command="{Binding SelectNextCommand}" />
        <KeyBinding Key="Up"
                    Command="{Binding SelectPreviousCommand}" />
      </TextBox.InputBindings>
    </TextBox>

    <Popup IsOpen="True"
           Opened="AdjustFocus_OnOpened"
           StaysOpen="False"
           Placement="Top"
           PlacementTarget="{Binding ElementName=EditMessageTextBox}">
      <ListView IsSynchronizedWithCurrentItem="True"
                Height="400"
                SelectedItem="{Binding SelectedDataItem}"
                ItemsSource="{Binding DataItems}"
                SelectionChanged="AdjustFocus_OnSelectionChanged">
        <ListView.ItemTemplate>
          <DataTemplate DataType="{x:Type DataItem}">
            <TextBox Text="{Binding FullName}" />
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
    </Popup>
  </Grid>
</Window>

备注

由于导航是使用 ItemsSourceCollectionView 完成的,因此 ListView.IsSynchronizedWithCurrentItem 属性 必须设置为 true。否则 CollectionView 的导航不会影响视图。