question

StashyCode-7539 avatar image
0 Votes"
StashyCode-7539 asked StashyCode-7539 commented

DataGrid TextBox Search using Ctrl + F and F3

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-wpfdotnet-wpf-xaml
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

PeterFleischer-3316 avatar image
1 Vote"
PeterFleischer-3316 answered StashyCode-7539 commented

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



x.gif (199.4 KiB)
· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Hi Peter, thank you for the brilliant solution! I just have a quick question - is there any way to click on Ctrl and F and the window to appear if I haven't clicked on a cell before that? I tried it and the window appeared only when I clicked on a random cell beforehand, but if I navigate to the page and the first thing I do is Ctrl+F nothing happens.

0 Votes 0 ·
PeterFleischer-3316 avatar image
0 Votes"
PeterFleischer-3316 answered StashyCode-7539 commented

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?

· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Hi Peter,
upon rendering the page, I'm not sure which element has focus. Maybe no element yet?

0 Votes 0 ·

Since these are DataGrid behaviours, they will occur only if an element of the DataGrid has focus, right?

0 Votes 0 ·
PeterFleischer-3316 avatar image
1 Vote"
PeterFleischer-3316 answered StashyCode-7539 commented

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
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Either this or <Page FocusManager.FocusedElement="{Binding ElementName=GridName}">, thanks a lot!

0 Votes 0 ·