DataGrid TextBox Search using Ctrl + F and F3

StashyCode 61 Reputation points
2021-11-11T14:05:22.47+00:00

I am building a WPF MVVM application.

I have a DataGrid, like so:

<DataGrid 
       Name=“Employees” 
       AutoGenerateColumns=“False”         
       CanUserAddRows=“False” 
       EnableColumnVirtualization=“True” 
       EnableRowVirtualization=“True” 
       ItemsSource={Binding EmployeesCollectionView} 
       SelectionUnit=“FullRow”        
       VirtualizingPanel.VirtualizationMode=“Recycling” //... />

and a TextBox inside one of the column's header:

<TextBox
       x:Name="tbSearchName"
       Text="{Binding DataContext.SearchName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=UserControl}}"
       TextWrapping="Wrap" />

ViewModel.cs:

public ObservableCollection<Employee> Employees
{
      get => employees;
      set
      {
           //set with NotifyPropertyChanged
           EmployeesCollectionView = CollectionViewSource.GetDefaultView(employees);
      }
}

public ICollectionView EmployeesCollectionView
{
      get => employeesCollectionView;
      set => //set with NotifyPropertyChanged
}

public string SearchName
{
      get => searchName;
      set =>  //set with NotifyPropertyChanged
}

I want to implement a Ctrl+F and F3 search (similar like the browser one). This means:
Ctrl+F - tbSearchName becomes active and the user starts typing
On every key press SearchName is updated
The first occurrence of SearchName is highlighted and when clicking on F3 it navigates to the next appearance and highlights it, and so on
If there are no occurrences, the tbSearchName's border turns red

What is the best way to implement that? Does everything happen in the code-behind or is there a MVVM way to do that?

Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,751 questions
XAML
XAML
A language based on Extensible Markup Language (XML) that enables developers to specify a hierarchy of objects with a set of properties and logic.
800 questions
0 comments No comments
{count} votes

Accepted answer
  1. Peter Fleischer (former MVP) 19,316 Reputation points
    2021-11-12T06:27:36.467+00:00

    Hi Stashy,
    try following demo:

    XAML:

    <Window x:Class="WpfApp1.Window082"  
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"  
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
            xmlns:local="clr-namespace:Wpf1App082"  
            xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"  
            mc:Ignorable="d"  
            Title="211111_StashyCode-7539" Height="450" Width="800">  
      <Window.DataContext>  
        <local:ViewModel/>  
      </Window.DataContext>  
      <StackPanel>  
        <DataGrid Name="Employees" Height="200"  
                  AutoGenerateColumns="False"  
                  CanUserAddRows="False"  
                  EnableColumnVirtualization="True"   
                  EnableRowVirtualization="True"  
                  ItemsSource="{Binding EmployeesDataTable}"  
                  SelectionUnit="CellOrRowHeader"  
                  VirtualizingPanel.VirtualizationMode="Recycling"  
                  IsSynchronizedWithCurrentItem="True">  
          <i:Interaction.Behaviors>  
            <local:DataGridBehavior/>  
          </i:Interaction.Behaviors>  
          <DataGrid.Columns>  
            <DataGridTextColumn Header="ID" Binding="{Binding ID}"/>  
            <DataGridTextColumn Width="100"/>  
            <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>  
            <DataGridTextColumn Width="*"/>  
          </DataGrid.Columns>  
        </DataGrid>  
        <Label Content="{Binding Info}"/>  
      </StackPanel>  
    </Window>  
    

    and code:

    using System;  
    using System.Collections.ObjectModel;  
    using System.ComponentModel;  
    using System.Linq;  
    using System.Runtime.CompilerServices;  
    using System.Windows;  
    using System.Windows.Controls;  
    using System.Windows.Data;  
    using System.Windows.Input;  
    using System.Windows.Interactivity;  
      
    namespace Wpf1App082  
    {  
      public class ViewModel : INotifyPropertyChanged  
      {  
      
        public ViewModel()  
        {  
          // Load demo data  
          for (int i = 0; i < 1000; i++) col.Add(new Data() { ID = i, Name = $"Row {i:0000}" });  
          cvs.Source = col;  
        }  
      
        private CollectionViewSource cvs = new CollectionViewSource();  
        private ObservableCollection<Data> col = new ObservableCollection<Data>();  
      
        public ICollectionView EmployeesDataTable { get => cvs.View; }  
      
        private string _searchName;  
        public string SearchName  
        {  
          get => this._searchName;  
          set  
          {  
            this._searchName = value;  
            SelItems = new ObservableCollection<Data>(col.Where((d) => d.Name.Contains(value)));  
            IndexOfSelItem = 0;  
            NextItem();  
          }  
        }  
        private ObservableCollection<Data> SelItems { get; set; }  
        private int IndexOfSelItem = 0;  
        DataGrid AssociatedObject = null;  
      
        private Window SearchWindow = null;  
        public void StartSearch(DataGrid dg)  
        {  
          if (SearchWindow == null)  
          {  
            AssociatedObject = dg;  
            SearchWindow = new Window() { Title = "Search", Height = 60, Width = 250, DataContext = this };  
            SearchWindow.Closing += (s, e) => SearchWindow = null;  
            SearchWindow.PreviewKeyDown += (s, e) => { if (e.Key == Key.F3) NextItem(); };  
            TextBox tb = new TextBox();  
            tb.SetBinding(TextBox.TextProperty, new Binding("SearchName") { UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, Mode = BindingMode.TwoWay });  
            SearchWindow.Content = tb;  
            SearchWindow.Show();  
            tb.Focus();  
          }  
          SearchWindow.Focus();  
          SearchWindow.WindowState = WindowState.Normal;  
          // log  
          Info = $"{DateTime.Now.ToLongTimeString()} StartSearch";  
        }  
      
        public void NextItem()  
        {  
          if (AssociatedObject != null && SelItems != null && SelItems.Count > IndexOfSelItem)  
          {  
            cvs.View.MoveCurrentTo(SelItems[IndexOfSelItem]);  
            var current = EmployeesDataTable.CurrentItem;  
            AssociatedObject.ScrollIntoView(current);  
            if (IndexOfSelItem++ >= SelItems.Count - 1) IndexOfSelItem = 0;  
          }  
          // log  
          Info = $"{DateTime.Now.ToLongTimeString()} NextItem";  
        }  
      
        private string _info;  
        public string Info  
        {  
          get => this._info;  
          set { this._info = value; OnPropertyChanged(); }  
        }  
      
        public event PropertyChangedEventHandler PropertyChanged;  
        private void OnPropertyChanged([CallerMemberName] string propName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));  
      }  
      
      public class Data  
      {  
        public int ID { get; set; }  
        public string Name { get; set; }  
      }  
      
      public class DataGridBehavior : Behavior<DataGrid>  
      {  
        protected override void OnAttached()  
        {  
          AssociatedObject.PreviewKeyDown += OnPreviewKeyDown;  
          AssociatedObject.PreviewKeyUp += OnPreviewKeyUp;  
        }  
      
        private void OnPreviewKeyDown(object sender, KeyEventArgs e)  
        {  
          if (e.Key == Key.F3) ((ViewModel)(AssociatedObject.DataContext)).NextItem();  
          else if (e.Key == Key.LeftCtrl) LeftCtrl = true;  
          else if (e.Key == Key.F) ((ViewModel)(AssociatedObject.DataContext)).StartSearch(AssociatedObject);  
        }  
      
        private void OnPreviewKeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.LeftCtrl) LeftCtrl = false; }  
      
        private bool LeftCtrl = false;  
      }  
    }  
    

    Result:

    148680-x.gif

    1 person found this answer helpful.

2 additional answers

Sort by: Most helpful
  1. Peter Fleischer (former MVP) 19,316 Reputation points
    2021-11-12T16:46:20.827+00:00

    Hi Stashy,
    you can set focus when DataGrid is loaded:

      public class DataGridBehavior : Behavior<DataGrid>
      {
        protected override void OnAttached()
        {
          AssociatedObject.PreviewKeyDown += OnPreviewKeyDown;
          AssociatedObject.PreviewKeyUp += OnPreviewKeyUp;
          AssociatedObject.Loaded += (s, e) => AssociatedObject.Focus(); ;
        }
    
        private void OnPreviewKeyDown(object sender, KeyEventArgs e)
        {
          if (e.Key == Key.F3) ((ViewModel)(AssociatedObject.DataContext)).NextItem();
          else if (e.Key == Key.LeftCtrl) LeftCtrl = true;
          else if (e.Key == Key.F) ((ViewModel)(AssociatedObject.DataContext)).StartSearch(AssociatedObject);
        }
    
        private void OnPreviewKeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.LeftCtrl) LeftCtrl = false; }
    
        private bool LeftCtrl = false;
      }
    
    1 person found this answer helpful.

  2. Peter Fleischer (former MVP) 19,316 Reputation points
    2021-11-12T14:57:37.897+00:00

    Hi Stashy,
    click Ctrl+F you can catch via PreviewKeyDown in element which has focus. Which element has the focus where you want to catch PreviewKeyDown?


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.