Search code examples
wpflistviewpopuplistviewitem

ListView Popup key functionality


I have implemented a listview that appears as a popup list. Now I would like to add key functionalities to it, like if whenever up arrow is pressed in a text box it should select an item in my list view and if pressing of KEY_UP/DOWN is continued it should continue changing its index respectively.

This is the EditMessageTextBox and associated EditMessageTagPopup enter image description here

This is the XAML code used:

<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>

and here is code behind:

 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;
                    }
                });

I have tried adding an item in scroll into view() instead of selected index yet no update

when there is a perfect selection made this function is invoked from code behind

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

and

Here is working functionality


Solution

  • The problem is that after each navigation to the next item of the ListView you want to set focus to the selection TextBox which binds to the SelectedItem. Otherwise navigating the items of a ListView with the help of the arrow keys is already the default behavior of the ListView.

    The simplest solution is to capture the keyboard input using UIElement.InputBinding on the selection TextBox (which enables to handle the keys in the view model) and then

    1. Select the next/previous item
    2. Scroll the SelectedItem into view
    3. Move the focus to the selection TextBox
    4. Move the caret of the selection TextBox to the end

    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>
    

    Remarks

    As the navigation is done using the CollectionView of the ItemsSource the ListView.IsSynchronizedWithCurrentItem property must be set to true. Otherwise the navigation of the CollectionView won't effect the view.