wpftextboxwpf-controlswpf-4.0

TextBox template padding issue


I am trying to re-template a TextBox to have two borders with padding in-between; however, even when I explicitly set the padding on the PART_ContentHost to zero, the padding for the control is always being applied to the inner ScrollViewer as well.

Small example:

<Style TargetType="{x:Type TextBox}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBox}">
                <Border Padding="{TemplateBinding Padding}"
                        BorderThickness="1"
                        BorderBrush="Black"
                        Background="LightBlue">
                    <ScrollViewer Padding="0" Margin="0" Background="Turquoise"
                                  x:Name="PART_ContentHost" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<TextBox Padding="15"/>

The result is a textbox that looks like: [15[15 Textfield 15]15]

while I expected just: [15[ Textfield ]15]

How do I properly force the PART_ContentHost ( scrollviewer / textfield) to have a zero padding?


Solution

  • This really looks like a weird bug to me unless someone has a better explanation for this behavior.

    ScrollViewer(PART_ContentHost) internally uses a Template like:

    <ControlTemplate TargetType="{x:Type ScrollViewer}">
      <Grid x:Name="Grid"
            Background="{TemplateBinding Background}">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*" />
          <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="*" />
          <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Rectangle x:Name="Corner"
                    Grid.Row="1"
                    Grid.Column="1"
                    Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
        <ScrollContentPresenter x:Name="PART_ScrollContentPresenter"
                                Grid.Row="0"
                                Grid.Column="0"
                                Margin="{TemplateBinding Padding}"
                                CanContentScroll="{TemplateBinding CanContentScroll}"
                                CanHorizontallyScroll="False"
                                CanVerticallyScroll="False"
                                Content="{TemplateBinding Content}"
                                ContentTemplate="{TemplateBinding ContentTemplate}" />
        <ScrollBar x:Name="PART_VerticalScrollBar"
                    Grid.Row="0"
                    Grid.Column="1"
                    AutomationProperties.AutomationId="VerticalScrollBar"
                    Cursor="Arrow"
                    Maximum="{TemplateBinding ScrollableHeight}"
                    Minimum="0"
                    ViewportSize="{TemplateBinding ViewportHeight}"
                    Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
                    Value="{Binding VerticalOffset,
                                    Mode=OneWay,
                                    RelativeSource={RelativeSource TemplatedParent}}" />
        <ScrollBar x:Name="PART_HorizontalScrollBar"
                    Grid.Row="1"
                    Grid.Column="0"
                    AutomationProperties.AutomationId="HorizontalScrollBar"
                    Cursor="Arrow"
                    Maximum="{TemplateBinding ScrollableWidth}"
                    Minimum="0"
                    Orientation="Horizontal"
                    ViewportSize="{TemplateBinding ViewportWidth}"
                    Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
                    Value="{Binding HorizontalOffset,
                                    Mode=OneWay,
                                    RelativeSource={RelativeSource TemplatedParent}}" />
      </Grid>
    </ControlTemplate>
    

    The interesting bit is:

    <ScrollContentPresenter x:Name="PART_ScrollContentPresenter"
                            Grid.Row="0"
                            Grid.Column="0"
                            Margin="{TemplateBinding Padding}"
                            CanContentScroll="{TemplateBinding CanContentScroll}"
                            CanHorizontallyScroll="False"
                            CanVerticallyScroll="False"
                            Content="{TemplateBinding Content}"
                            ContentTemplate="{TemplateBinding ContentTemplate}" />
    

    Now to fix your issue you can just set the Margin there to 0 instead of {TemplateBinding Padding} and you'll get your desired output.

    But why did we need to do that?

    TemplateBinding Padding seems to be ignoring the value set directly on the ScrollViewer which is in the inner scope and picks the Padding value inherited from the Parent(Button) which is 15.

    Ok that's weird, but what's worse is it's only for Padding. Foreground, Background, Margin are all fine when set directly on ScrollViewer they override the TextBox's fields. I even to confirm moved the Padding set directly on the TextBox in usage to a default Style setter, to see if some precedence case was the problem.

    It did not seem to be. Got same output.

    Padding is defined in System.Windows.Controls.Control which is the same class Foreground and Background are from that ScrollViewer inherits. Not sure why padding alone is behaving differently.

    I also tried changing the presenter to something like

    <ScrollContentPresenter x:Name="PART_ScrollContentPresenter"
                            Grid.Row="0"
                            Grid.Column="0"
                            Margin="{TemplateBinding Margin}"
                            CanContentScroll="{TemplateBinding CanContentScroll}"
                            CanHorizontallyScroll="False"
                            CanVerticallyScroll="False"
                            Content="{TemplateBinding Padding}"
                            ContentTemplate="{TemplateBinding ContentTemplate}" />
    

    It print's 15,15,15,15. Doesn't do that for the Margin.

    Same effect with a {Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}, Path=Padding} binding.

    I saw one post saying ScrollViewer does not pass properties set on it to it's children. Don't really get that since if that was the case how did Background, Margin and sorts be fine to over-ride? Whats so special about Padding? If it is valid behavior, I don't really see how to get rid of that behavior without Templating the ScrollViewer as well and it's one confusing implementation.