There is no standard way in WPF that provides this specific functionality out-of-the-box. However, there are several ways this can be done.
One way I can think of, which should be relatively easy to implement, is to use attached properties. Using attached properties, you would have to opt in on each label you wish to find text.
Because each control may use different properties to set their text, you may prefer to have a way of defining what text is to be found on the control. For example,
TextBlock
objects use
Text
property, while
Button
objects use
Content
property, which may or may not be a
string
. To make this work, you can create an attached property on the
Window
(or some other class) that accepts a
string
and set that attached property on each control with a binding to the actual text property like:
<TextBlock Text="Some text" MyWindow.MyAttachedProperty="{Binding Text, RelativeSource={RelativeSource Mode=Self}}"/>
Although the solution I mention above is versatile, it may not be the most practical and requires you writing a binding for each findable control. If you only wish to find specific control types, like
Label
,
TextBlock
and
Button
, for instance, you would simply create an inheritable boolean attached property that accepts a
UIElement
. Then you would use like this:
<TextBlock Text="Some text" MyWindow.IsFindable="True"/>
Or, because
MyWindow.IsFindable
property would be inheritable, you could set it only once on the parent container:
<Grid MyWindow.IsFindable="True">
<TextBlock Text="Some text"/>
<Button Content="Button text"/>
</Grid>
Now, how would you implement such attached property? Well, you could set up a static
HashSet<UIElement>
instance that stores the findable
UIElement
instances, like the following:
private static readonly Lazy<HashSet<UIElement>> _findableElements = new();
private static HashSet<UIElement> FindableElements => _findableElements.Value;
public static readonly DependencyProperty IsFindableProperty
= DependencyProperty.RegisterAttached("IsFindable", typeof(bool), typeof(MyWindow),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.Inherits,
new PropertyChangedCallback(IsFindablePropertyChanged)));
private static void IsFindablePropertyChanged(
DependencyObject dObj, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)dObj;
bool value = (bool)e.NewValue;
if (value)
{
FindableElements.Add(element);
}
else
{
FindableElements.Remove(element);
}
}
public static void SetIsFindable(UIElement element, bool isFindable)
{
element?.SetValue(IsFindableProperty, isFindable) ?? throw new ArgumentNullException(nameof(element));
}
public static bool GetIsFindable(UIElement element)
{
(bool)(element?.GetValue(IsFindableProperty) ?? throw new ArgumentNullException(nameof(element)));
}
public static IEnumerable<UIElement> FindInElements(Predicate<string> predicate)
{
if (predicate is null)
throw new ArgumentNullException(nameof(predicate));
foreach (var element in FindableElements)
{
string elementText = GetTextFromElement(element);
if (!string.IsNullOrEmpty(elementText) && predicate.Invoke(elementText))
{
yield return element;
}
}
}
private static string GetTextFromElement(UIElement element)
{
if (element is TextBlock textBlock)
return textBlock.Text;
else if (element is ContentElement contentElement)
return contentElement.Content as string;
else
return null;
}
The code above would be inside your window (the example assumes
MyWindow
), but could be inside any other container. Because the attached property is inheritable, you would set
IsFindable="True"
only on the window and still opting out any element inside by setting
IsFindable="False"
on that specific element.
To find elements, you would then use code like this:
string query = "Some text to find";
foreach (var element in FindInElements(text => text.Contains(query)))
{
}
There are several other ways of achieving similar results, and this is just one of them. You can traverse the logical tree, you can use attached properties differently, etc.