How do I change the style of FindTextBox of FlowDocumentReader to match the theme of my app?

Viewed 114

I have a WPF app with a FlowDocumentReader control and when I click the search button to search the text, the text box doesn't match the theme of the app. The text does, but the background of the text box doesn't. So if I am using a dark theme, the text I type into the text box is white (which is correct), but the text box background is also white (incorrect, making text not legible).

Does anyone know how to fix this? I tried applying a style but I don't know which component to target.

Wrong text box color

1 Answers

Wow, Microsoft really does not make this easy.


My Process

I tried the easy trick of adding a Style TargeType="TextBox" to FlowDocumentReader.Resources, but that doesn't work.

I tried doing things the "right" way and overriding FlowDocumentReader's ControlTemplate, but the TextBox in question isn't even part of the ControlTemaplte! Instead, there's a Border named PART_FindToolBarHost. The TextBox we want is added as a child to PART_FindToolBarHost in code- but only after the user has clicked the "find" button (the one with the magnifying glass icon). You can see this for yourself by looking at the control's source code.

With no more XAML-only ideas, I had to resort to using code. We need to somehow get a reference to the TextBox being created, and I can't think of any better way than to manually search the visual tree for it. This is complicated by the fact that the TextBox only exists once the find command has been executed.

Specifically, the find button in FlowDocumentReader binds to ApplicationCommands.Find. I tried adding a CommandBinding for that command to FlowDocumentReader so I could use it as a trigger to retrieve the TextBox. Unfortunately, adding such a CommandBinding somehow breaks the built-in functionality and prevents the TextBox from being generated at all. This breaks even if you set e.Handled = False.

Luckily, though, FlowDocumentReader exposes an OnFindCommand- except, of course, it's a Protected method. So I finally gave in and decided to inherit FlowDocumentReader. OnFindCommand works as a reliable trigger, but it turns out the TextBox isn't created until after the sub finishes. I was forced to use Dispatcher.BeginInvoke in order to schedule a method to run after the TextBox was actually added.

Using OnFindCommand as a trigger, I was finally able to reliably get a reference to the "find" TextBox, which is actually named FindTextBox.

Now that I can get a reference, we can apply our own Style. Except: FindTextBox already has a Style, so unless we want to override it, we're going to have to merge the two Styles. There's no publicly-accessible method for this (even though WPF does this internally in places), but luckily I already had some code for this.


The Working Code

First, a Module with the helper methods I used:

FindVisualChild is used to loop through the visual tree and get a reference to FindTextBox.
MergeStyles is used to combine the existing Style with the Style we supply, once we have that reference.

Module OtherMethods
    <Extension()>
    Public Function FindVisualChild(obj As DependencyObject, Name As String) As FrameworkElement
        For i As Integer = 0 To VisualTreeHelper.GetChildrenCount(obj) - 1
            Dim ChildObj As DependencyObject = VisualTreeHelper.GetChild(obj, i)

            If TypeOf ChildObj Is FrameworkElement AndAlso DirectCast(ChildObj, FrameworkElement).Name = Name Then Return ChildObj

            ChildObj = FindVisualChild(ChildObj, Name)
            If ChildObj IsNot Nothing Then Return ChildObj
        Next
        Return Nothing
    End Function

    Public Function MergeStyles(ByVal style1 As Style, ByVal style2 As Style) As Style
        Dim R As New Style

        If style1 Is Nothing Then Throw New ArgumentNullException("style1")
        If style2 Is Nothing Then Throw New ArgumentNullException("style2")
        If style2.BasedOn IsNot Nothing Then style1 = MergeStyles(style1, style2.BasedOn)

        For Each currentSetter As SetterBase In style1.Setters
            R.Setters.Add(currentSetter)
        Next

        For Each currentTrigger As TriggerBase In style1.Triggers
            R.Triggers.Add(currentTrigger)
        Next

        For Each key As Object In style1.Resources.Keys
            R.Resources(key) = style1.Resources(key)
        Next

        For Each currentSetter As SetterBase In style2.Setters
            R.Setters.Add(currentSetter)
        Next

        For Each currentTrigger As TriggerBase In style2.Triggers
            R.Triggers.Add(currentTrigger)
        Next

        For Each key As Object In style2.Resources.Keys
            R.Resources(key) = style2.Resources(key)
        Next

        Return R
    End Function
End Module

Then, there's StyleableFlowDocumentReader, which is what I named my extended control that inherits FlowDocumentReader:

Public Class StyleableFlowDocumentReader
    Inherits FlowDocumentReader

    Protected Overrides Sub OnFindCommand()
        MyBase.OnFindCommand()
        Dispatcher.BeginInvoke(Sub() GetFindTextBox(), DispatcherPriority.Render)
    End Sub

    Private Sub GetFindTextBox()
        findTextBox = Me.FindVisualChild("FindTextBox")
        ApplyFindTextBoxStyle()
    End Sub

    Private Sub ApplyFindTextBoxStyle()
        If findTextBox IsNot Nothing Then
            If findTextBox.Style IsNot Nothing AndAlso FindTextBoxStyle IsNot Nothing Then
                findTextBox.Style = MergeStyles(findTextBox.Style, FindTextBoxStyle)
            Else
                findTextBox.Style = If(FindTextBoxStyle, findTextBox.Style)
            End If
        End If
    End Sub

    Private findTextBox As TextBox

    Public Property FindTextBoxStyle As Style
        Get
            Return GetValue(FindTextBoxStyleProperty)
        End Get
        Set(ByVal value As Style)
            SetValue(FindTextBoxStyleProperty, value)
        End Set
    End Property
    Public Shared ReadOnly FindTextBoxStyleProperty As DependencyProperty =
                           DependencyProperty.Register("FindTextBoxStyle",
                           GetType(Style), GetType(StyleableFlowDocumentReader),
                           New PropertyMetadata(Nothing, Sub(d, e) DirectCast(d, StyleableFlowDocumentReader).ApplyFindTextBoxStyle()))
End Class

And then, finally, a usage example:

<local:StyleableFlowDocumentReader x:Name="Reader">
    <local:StyleableFlowDocumentReader.FindTextBoxStyle>
        <Style TargetType="TextBox">
            <Setter Property="Foreground" Value="Blue"/>
        </Style>
    </local:StyleableFlowDocumentReader.FindTextBoxStyle>
        
    <FlowDocument/>
</local:StyleableFlowDocumentReader>
Related