TextBox template padding issue

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.


This weird behavior is coming from TextBoxBase. It overrides metadata for some of its dependency properties and for the Padding property it looks like:

Control.PaddingProperty.OverrideMetadata(
    typeof (TextBoxBase), 
    new FrameworkPropertyMetadata(
        new PropertyChangedCallback(TextBoxBase.OnScrollViewerPropertyChanged)));

If you take a look at the OnScrollViewerPropertyChanged handler, you'll notice that it passes the value of the changed property to its ScrollViewer:

  if (newValue == DependencyProperty.UnsetValue)
    textBoxBase.ScrollViewer.ClearValue(e.Property);
  else
    textBoxBase.ScrollViewer.SetValue(e.Property, newValue);

So, no matter what Padding value you set in Control Template, it will be overwritten with the local value by TextBox in runtime.

To compensate this padding you can set a negative margin to ScrollViewer in the template:

<ScrollViewer 
    x:Name="PART_ContentHost" 
    Margin="{TemplateBinding Padding, Converter={StaticResource InvertThicknessConverter}}"
/>

where InvertThicknessConverter is a value converter which negates every component of the passed thickness value:

public class InvertThicknessConverter: IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is Thickness) return InvertThickness((Thickness)value);
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is Thickness) return InvertThickness((Thickness)value);
        return value;
    }

    private static Thickness InvertThickness(Thickness value)
    {
        return new Thickness(-value.Left, -value.Top, -value.Right, -value.Bottom);
    }
}