0

I have a WPF MVVM application with a dynamic UI (meaning there may be many different elements shown within a certain User Control).

I'd like to add the ability for the user to search the whole screen to find the text they are looking for... similar to how one would use the web browser built-in search capabilities.

I've not been able to find a universal solution for this and am thinking of searching the visual tree for displayed text, but I'm hopeful, since this seems like a general purpose need, that I'm missing some relatively out of the box solution here.

Christoph
  • 4,251
  • 3
  • 24
  • 38

1 Answers1

0

This is tricky.
For now it's only a part of the solution but here is an example to highlight a search in all Textblocks of in the VisualTree of a root object.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

public class GlobalTextHighlighter
{
    public static Brush GetHighlightBackground(DependencyObject obj)
    {
        return (Brush)obj.GetValue(HighlightBackgroundProperty);
    }

    public static void SetHighlightBackground(DependencyObject obj, Brush value)
    {
        obj.SetValue(HighlightBackgroundProperty, value);
    }

    // Using a DependencyProperty as the backing store for HighlightBackground.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty HighlightBackgroundProperty =
        DependencyProperty.RegisterAttached("HighlightBackground", typeof(Brush), typeof(GlobalTextHighlighter), new PropertyMetadata(Brushes.Yellow, new PropertyChangedCallback(RefreshHighlighting)));

    public static Brush GetHighlightForeground(DependencyObject obj)
    {
        return (Brush)obj.GetValue(HighlightForegroundProperty);
    }

    public static void SetHighlightForeground(DependencyObject obj, Brush value)
    {
        obj.SetValue(HighlightForegroundProperty, value);
    }

    // Using a DependencyProperty as the backing store for HighlightForeground.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty HighlightForegroundProperty =
        DependencyProperty.RegisterAttached("HighlightForeground", typeof(Brush), typeof(GlobalTextHighlighter), new PropertyMetadata(Brushes.Black, new PropertyChangedCallback(RefreshHighlighting)));

    public static string GetSearchText(DependencyObject obj)
    {
        return (string)obj.GetValue(SearchTextProperty);
    }

    public static void SetSearchText(DependencyObject obj, string value)
    {
        obj.SetValue(SearchTextProperty, value);
    }

    // Using a DependencyProperty as the backing store for SearchText.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SearchTextProperty =
        DependencyProperty.RegisterAttached("SearchText", typeof(string), typeof(GlobalTextHighlighter), new PropertyMetadata(string.Empty, new PropertyChangedCallback(RefreshHighlighting)));

    private static void RefreshHighlighting(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if(d != null)
        {
            try
            {
                Brush background = (Brush)d.GetValue(HighlightBackgroundProperty);
                Brush foreground = (Brush)d.GetValue(HighlightForegroundProperty);
                string highlightText = (string)d.GetValue(SearchTextProperty);

                FindVisualChildren<TextBlock>(d)
                    .ToList()
                    .ForEach(textBlock =>
                    {
                        try
                        {
                            string text = textBlock.Text;

                            if (!string.IsNullOrEmpty(text))
                            {
                                if (string.IsNullOrEmpty(highlightText))
                                {
                                    textBlock.Text = text;
                                }
                                else
                                {
                                    int index = text.IndexOf(highlightText, StringComparison.CurrentCultureIgnoreCase);

                                    if (index < 0)
                                        textBlock.Text = text;
                                    else
                                        textBlock.Inlines.Clear();

                                    while (index >= 0)
                                    {
                                        textBlock.Inlines.AddRange(new Inline[]
                                        {
                                            new Run(text.Substring(0, index)),
                                            new Run(text.Substring(index, highlightText.Length))
                                            {
                                                Background = background,
                                                Foreground = foreground
                                            }
                                        });

                                        text = text.Substring(index + highlightText.Length);
                                        index = text.IndexOf(highlightText, StringComparison.CurrentCultureIgnoreCase);

                                        if (index < 0)
                                        {
                                            textBlock.Inlines.Add(new Run(text));
                                        }
                                    }
                                }
                            }
                        }
                        catch
                        { }
                    });
            }
            catch { }
        }
    }

    public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
    {
        if (depObj != null)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                if (child != null && child is T)
                {
                    yield return (T)child;
                }

                foreach (T childOfChild in FindVisualChildren<T>(child))
                {
                    yield return childOfChild;
                }
            }
        }
    }
}

And you can use it on the root object from where you want to search.

Example :

<TextBox x:Name="GlobalSearchTextBox" 
         Text="{Binding GlobalSearch, UpdateSourceTrigger=PropertyChanged, Delay=100}"/>
<DockPanel local:GlobalTextHighlighter.SearchText="{Binding GlobalSearch}"
           local:GlobalTextHighlighter.HighlightBackground="Orange"
           local:GlobalTextHighlighter.HighlightForeground="Blue">
    <!-- ... -->
</DockPanel>

It's based on WPF TextBlock Highlighter and Find all controls in WPF Window by type.

As I said it's not yet a complete solution.
And it comes with some drawbacks.

  • It doesn't work with all components that use AccessText like Labels.
  • It doesn't work with editing components like TextBoxs or RichTextBox.
  • It's not browsing from one match to an other.
    • If you want that you need to manage nested ScrollAreas.

I hope it already helps to approach the solution.

Coding Seb
  • 83
  • 1
  • 5