Why does an implicit TextBlock style get applied when binding Label.Content to a non-string, but not a string?

倖福魔咒の 提交于 2019-12-03 07:58:08

问题


I was looking at this question, and discovered that binding Label.Content to a non-string value will apply an implicit TextBlock style, however binding to a string does not.

Here's some sample code to reproduce the problem:

<Window.Resources>
    <Style TargetType="Label">
        <Setter Property="FontSize" Value="26"/>
        <Setter Property="Margin" Value="10"/>
        <Setter Property="VerticalAlignment" Value="Center"/>
    </Style>
    <Style TargetType="{x:Type TextBlock}">
        <Setter Property="FontSize" Value="26"/>
        <Setter Property="Margin" Value="10"/>
    </Style>
</Window.Resources>

<Grid>
    <StackPanel Orientation="Horizontal">
        <Label Content="{Binding SomeString}" Background="Red"/>
        <Label Content="{Binding SomeDecimal}" Background="Green"/>
    </StackPanel>
</Grid>

Where the code for the bound values are

SomeDecimal = 50;
SomeString = SomeDecimal.ToString();

And the end result looks like this, with the Margin property from the implicit TextBlock style getting applied to the Label bound to a non-string only:

Both labels get rendered as

<Label>
    <Border>
        <ContentPresenter>
            <TextBlock />
        </ContentPresenter>
    </Border>
</Label>

When I check out the VisualTree with Snoop, I can see that it looks exactly the same for both elements, except the 2nd TextBlock applies the Margin from the implicit style, while the first does not.

I've used Blend to pull out a copy of the default Label Template, but don't see anything strange there, and when I apply the template to both my labels, the same thing happens.

<Label.Template>
    <ControlTemplate TargetType="{x:Type Label}">
        <Border BorderBrush="{TemplateBinding BorderBrush}" 
                BorderThickness="{TemplateBinding BorderThickness}" 
                Background="{TemplateBinding Background}" 
                Padding="{TemplateBinding Padding}" 
                SnapsToDevicePixels="True">
            <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" 
                              Content="{TemplateBinding Content}" 
                              ContentStringFormat="{TemplateBinding ContentStringFormat}" 
                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                              RecognizesAccessKey="True" 
                              SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</Label.Template> 

It should also be noted that setting a default ContentTemplate to a TextBlock does make both items render without the implicit style, so it must have something to do with when WPF tries to render a non-string value as part of the UI.

<Window.Resources>
    <Style TargetType="Label">
        <Setter Property="FontSize" Value="26"/>
        <Setter Property="Margin" Value="10"/>
        <Setter Property="VerticalAlignment" Value="Center"/>
    </Style>
    <Style x:Key="TemplatedStyle" TargetType="Label" BasedOn="{StaticResource {x:Type Label}}">
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <TextBlock Text="{Binding }"/>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="{x:Type TextBlock}">
        <Setter Property="FontSize" Value="26"/>
        <Setter Property="Margin" Value="10"/>
    </Style>
</Window.Resources>

<Grid>
    <StackPanel Orientation="Horizontal">
        <Label Content="{Binding SomeString}" Background="Red"/>
        <Label Content="{Binding SomeDecimal}" Background="Green"/>
        <Label Content="{Binding SomeString}" Background="Red" 
               Style="{StaticResource TemplatedStyle}"/>
        <Label Content="{Binding SomeDecimal}" Background="Green" 
               Style="{StaticResource TemplatedStyle}"/>
    </StackPanel>
</Grid>

What is the logic that causes a non-string inserted into the UI to be drawn using an implicit TextBlock style, but a string inserted into the UI does not? And where does this occur at?


回答1:


EDIT: (maybe move this to the bottom?)

And I poked a bit more - and I think I got to the crux of the problem (w/ emphasis on 'I think')

Put this into some Button1_Click or something (again, we need to go 'lazy' on this - as we need the visual tree constructed - we cannot do it on 'Loaded' as we just made the templates - this required better initialization technique true, but it's just a test so who cares)

void Button_Click(object sender, EventArgs e)
{
var insideTextBlock = FindVisualChild<TextBlock>(_labelString);
var value = insideTextBlock.GetProperty<bool>("HasImplicitStyleFromResources"); // false
value = insideTextBlock.GetProperty<bool>("ShouldLookupImplicitStyles"); // true

var boundaryElement = insideTextBlock.TemplatedParent; // ContentPresenter and != null

insideTextBlock = FindVisualChild<TextBlock>(_labelDecimal);
value = insideTextBlock.GetProperty<bool>("HasImplicitStyleFromResources"); // true
value = insideTextBlock.GetProperty<bool>("ShouldLookupImplicitStyles"); // true

boundaryElement = insideTextBlock.TemplatedParent; // == null !!

As mentioned here Implicit styles in Application.Resources vs Window.Resources?
The FindImplicitStyleResource (in FrameworkElement) uses something like...

boundaryElement = fe.TemplatedParent;  

And seems that if there is no TemplatedParent (and due to the ways the TextBlock is constructed within the DefaultTemplate) - there are no 'boundaries' set - and search for implicit resources / styles - propagates all the way.



Original Answer: (read this first if you just arrived)

(@dowhilefor and @Jehof already touched on the main things)
I'm not sure this is an 'answer' as such - it's still a guess work - but I needed more space to explain what I think is going on.

You can find the 'ContentPresenter source' code on the web - it's easier than using reflector - just 'google' for it, I'm not posting it here for the obvious reasons :)

It's about the ContentTemplate that is chosen for the ContentPresenter (and in this order)...

ContentTemplate // if defined 
ContentTemplateSelector // if defined
FindResource // for typeof(Content) - eg if defined for sys:Decimal takes that one
DefaultTemplate used internally by the presenter
...specific templates are chosen based on typeof(Content)

And indeed it doesn't have anything to do with the Label but any ContentControl or control template that uses ContentPresenter. Or you could bind to resource etc.

Here is a repro of what's going on inside - my goal was to reproduce similar behavior for 'strings' or any type of content.

In XAML just 'name' the labels (and it isn't a typo, a deliberately put strings in both to level the playing field sort of)...

<Label Name="_labelString" Content="{Binding SomeString}" Background="Red"/>
<Label Name="_labelDecimal" Content="{Binding SomeString}" Background="Green"/>

And from code behind (the minimal code that sort of mimics what presenter does):
note: I did it on Loaded as I needed access to the presenter implicitly created

void Window1_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(TextBlock));
factory.SetValue(TextBlock.TextProperty, new TemplateBindingExtension(ContentProperty));
var presenterString = FindVisualChild<ContentPresenter>(_labelString);
presenterString.ContentTemplate = new DataTemplate() { VisualTree = factory };

// return;

var presenterDecimal = FindVisualChild<ContentPresenter>(_labelDecimal);
presenterDecimal.ContentTemplate = new DataTemplate(); 
// just to avoid the 'default' template kicking in

// this is what 'default template' does actually, the gist of it
TextBlock textBlock = new TextBlock();
presenterDecimal.SetProperty(typeof(FrameworkElement), "TemplateChild", textBlock);
textBlock.Text = presenterDecimal.Content.ToString();

First part (for _labelString) does what 'text' template does for strings.

If you return right after that - you'll get the two same looking boxes, no implicit template.

Second part (for _labelDecimal) mimics the 'default template' which is invoked for the 'decimal'.

End result should behave the same as the original example. We constructed the templates as for the string and decimal - but we can put anything in the content (if it makes sense of course).

As to why - my guess is something like this (though far from certain - somebody will jump in with something more sensible I guess)...

As per this link FrameworkElementFactory

This class is a deprecated way to programmatically create templates, which are subclasses of FrameworkTemplate such as ControlTemplate or DataTemplate; not all of the template functionality is available when you create a template using this class. The recommended way to programmatically create a template is to load XAML from a string or a memory stream using the Load method of the XamlReader class.

And I'm guessing it doesn't invoke any defined styles for the TextBlock.

While the 'other template' (default template) - actually constructs the TextBlock and somewhere along those lines - it actually picks up the implicit style.

Frankly, that's as much as I was able to conclude, short of going through the entire WPF 'internals' and how/where actually styles get applied.


I used this code Finding control within WPF itemscontrol for FindVisualChild.
And the SetProperty is just the reflection - for that one property we need access to to be able to do all this. e.g.
public static void SetProperty<T>(this object obj, string name, T value) { SetProperty(obj, obj.GetType(), name, value); }
public static void SetProperty<T>(this object obj, Type typeOf, string name, T value)
{
    var property = typeOf.GetProperty(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
    property.SetValue(obj, value, null);
}



回答2:


After going through this question and valuable comments from all, I have done some research on TextBlock Styling.

To my understanding the problem here is not with the Label or TextBlock, it is with the contentpresenter and the controls which use contentpresenter like Label, button and ComboBoxItem.

One of the properties of content presenter from MSDN : http://msdn.microsoft.com/en-us/library/system.windows.controls.contentpresenter.aspx

" If there is a TypeConverter that converts the type of Content to a string, the ContentPresenter uses that TypeConverter and creates a TextBlock to contain that string. The TextBlock is displayed "

In the example above, For SomeString Content presenter is converting it into textblock and applying the TextBlock margin (10) along with Label margin (10) making it 20.

In order to avoid this scenario you need to override the TextBlock style in contentpresenter as shown below

                           <ContentPresenter >
                               <ContentPresenter.Resources>
                                    <Style TargetType="{x:Type TextBlock}">
                                        <Setter Property="Margin" Value="5" />
                                    </Style>
                                </ContentPresenter.Resources>
                            </ContentPresenter>

Following is the changes to your code.

    <Window.Resources>
        <Style TargetType="Label">
            <Setter Property="FontSize" Value="26"/>
            <Setter Property="Margin" Value="10"/>

            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Template">
                <Setter.Value>

                    <ControlTemplate TargetType="Label">
                        <Grid>
                            <Rectangle Fill="{TemplateBinding Background}" />
                            <ContentPresenter >
                                <ContentPresenter.Resources>
                                    <Style TargetType="{x:Type TextBlock}">
                                        <Setter Property="Margin" Value="5" />
                                    </Style>
                                </ContentPresenter.Resources>
                            </ContentPresenter>
                        </Grid>

                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="FontSize" Value="26"/>
            <Setter Property="Margin" Value="10"/>
            <Setter Property="Foreground" Value="Pink" />
        </Style>
    </Window.Resources>

    <Grid>
        <StackPanel Orientation="Horizontal">
            <Label Content="{Binding SomeString}" Background="Red" />
            <Label Content="{Binding SomeDecimal}" Background="Green"/>
        </StackPanel>
    </Grid>
</Window>

This explanation is just based on my understanding. Let me know your comments.

Thanks




回答3:


According to my comment i add more information to the question. Its not a direct answer, but provides additional information to the described problem.

The XAML below will display the described behavior directly in the Designer of Visual Studio and i have narrowed it down to the ContentPresenter, which seems to be source of the problem. The style gets applied to the first both ContentPresenter (intPresenter and boolPresenter), but not the last that uses a string as Content (stringPresenter).

<Window.Resources>
  <system:Int32 x:Key="intValue">5</system:Int32>
  <system:Boolean x:Key="boolValue">false</system:Boolean>
  <system:String x:Key="stringValue">false</system:String>
  <Style TargetType="{x:Type TextBlock}">
    <Setter Property="FontSize" Value="26" />
    <Setter Property="Margin" Value="10" />
  </Style>
</Window.Resources>

 <Grid>
   <StackPanel Orientation="Horizontal">
     <ContentPresenter x:Name="intPresenter" 
                       VerticalAlignment="Center"
                       Content="{StaticResource intValue}" />
     <ContentPresenter x:Name="boolPresenter" 
                       VerticalAlignment="Center"
                       Content="{StaticResource boolValue}" />
     <ContentPresenter x:Name="stringPresenter"
                       VerticalAlignment="Center"
                       Content="{StaticResource stringValue}" />
   </StackPanel>
  </Grid>

In the debugger i have analyzed that the stringPresenter uses the DefaultStringTemplate while the intPresenter does not.

Its also interesting that the Language of the intPresenter is set, while by the stringPresenter its not.

And the implementation of the method looks something like that (taken from dotPeek)

private bool IsUsingDefaultStringTemplate
    {
      get
      {
        if (this.Template == ContentPresenter.StringContentTemplate || this.Template == ContentPresenter.AccessTextContentTemplate)
          return true;
        DataTemplate dataTemplate1 = ContentPresenter.StringFormattingTemplateField.GetValue((DependencyObject) this);
        if (dataTemplate1 != null && dataTemplate1 == this.Template)
          return true;
        DataTemplate dataTemplate2 = ContentPresenter.AccessTextFormattingTemplateField.GetValue((DependencyObject) this);
        return dataTemplate2 != null && dataTemplate2 == this.Template;
      }
    }

The StringContentTemplate and AccessTextTemplate are using a FrameworkElementFactory to generate the Visuals.



来源:https://stackoverflow.com/questions/15951456/why-does-an-implicit-textblock-style-get-applied-when-binding-label-content-to-a

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!