开发者

XAML binding doesn't seem to set if the property is initialized in the constructor

I've run into a problem with data-binding inside control template while the property is initialized inside the constructor.

Here is the show-case (you can also download sample solution):

CustomControl1.cs

public class CustomControl1 : ContentControl
{
    static CustomControl1()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(CustomControl1), 
            new FrameworkPropertyMetadata(typeof(CustomControl1)));
    }

    public CustomControl1()
    {
        Content = "Initial"; // comment this line out and everything 
                             // will start working just great
    }
}

CustomControl1 style:

<Style TargetType="{x:Type local:CustomControl1}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:CustomControl1}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <ContentPresenter />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

CustomControl2.cs:

public class CustomControl2 : ContentControl
{
    static CustomControl2()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(CustomControl2), 
            new FrameworkPropertyMetadata(typeof(CustomControl2)));
    }
}

CustomControl style:

<Style TargetType="{x:Type local:CustomControl2}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:CustomControl2}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <local:CustomControl1 
                        Content="{Binding Content, 
                            RelativeSource={RelativeSource 
                                    AncestorType=local:CustomControl2}}" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Window1.xaml:

<Window x:Class="WpfApplication5.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300"
        xmlns:local="clr-namespace:WpfApplication5">
    <Grid>
        <local:CustomControl2 Content="Some content" />
    </Grid>
</Window>

So, the problem is: when you launch the app, the content of CustomControl1 appears to be "Initial" which is set by constructor, not the "Some content" string, which is supposed to be set by binding.

When we remove the initialization from the constructor, the binding starts working.

First of all, let me predict the answer: "you should set the initial value of a dependency property inside its metadata: either at the moment of registration or by means of metadata overriding capabilities". Yeap, you right, but the problem with this method of initialization is that the property is of collection type, so if I'll provide new MyCustomCollection() as a default value of the property, then every instance of CustomControl1 will share the same instance of that collection and that's obviously not the idea.

I've done some debugging on the problem, here are the results:

  • Binding instance is created, when we put it in element-like syntax and assign x:Name to it, then it's accessible through Template.FindName("PART_Binding", this) inside OnApplyTemplate.
  • Binding simply isn't set on the property: inside the same OnApplyTemplate the code this.GetBindingExpression(ContentProperty) return null.
  • There is nothing wrong with the binding itself: inside OnApplyTemplate we can look it up and then we can simply set it on the property like this: this.SetBinding(ContentProperty, myBinding) - everything will work fine.

Can anyone explain how and why that h开发者_运维问答appens?

Does anyone have a solution for setting non-shared initial value for a dependency property, so the binding wouldn't break?

Thanks in advance!

UPD: The most weird thing is that debug output with highest trace-level is the same for both cases: either when the binding doesn't occur or if it does.

Here it goes:

System.Windows.Data Warning: 52 : Created BindingExpression (hash=18961937) for Binding (hash=44419000)
System.Windows.Data Warning: 54 :   Path: 'Content'
System.Windows.Data Warning: 56 : BindingExpression (hash=18961937): Default mode resolved to OneWay
System.Windows.Data Warning: 57 : BindingExpression (hash=18961937): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 58 : BindingExpression (hash=18961937): Attach to WpfApplication5.CustomControl1.Content (hash=47980820)
System.Windows.Data Warning: 62 : BindingExpression (hash=18961937): RelativeSource (FindAncestor) requires tree context
System.Windows.Data Warning: 61 : BindingExpression (hash=18961937): Resolve source deferred
System.Windows.Data Warning: 63 : BindingExpression (hash=18961937): Resolving source 
System.Windows.Data Warning: 66 : BindingExpression (hash=18961937): Found data context element: <null> (OK)
System.Windows.Data Warning: 69 :     Lookup ancestor of type CustomControl2:  queried Border (hash=11653293)
System.Windows.Data Warning: 69 :     Lookup ancestor of type CustomControl2:  queried CustomControl2 (hash=54636159)
System.Windows.Data Warning: 68 :   RelativeSource.FindAncestor found CustomControl2 (hash=54636159)
System.Windows.Data Warning: 74 : BindingExpression (hash=18961937): Activate with root item CustomControl2 (hash=54636159)
System.Windows.Data Warning: 104 : BindingExpression (hash=18961937):   At level 0 - for CustomControl2.Content found accessor DependencyProperty(Content)
System.Windows.Data Warning: 100 : BindingExpression (hash=18961937): Replace item at level 0 with CustomControl2 (hash=54636159), using accessor DependencyProperty(Content)
System.Windows.Data Warning: 97 : BindingExpression (hash=18961937): GetValue at level 0 from CustomControl2 (hash=54636159) using DependencyProperty(Content): 'Some content'
System.Windows.Data Warning: 76 : BindingExpression (hash=18961937): TransferValue - got raw value 'Some content'
System.Windows.Data Warning: 85 : BindingExpression (hash=18961937): TransferValue - using final value 'Some content'

UPD2: added a link to the sample solution


See this answer: Binding Setting Property but UI not updating. Can I debug within referenced project/control?

Use SetCurrentValue() in constructor.


I've crossposted the problem at MSDN forums, someone there has suggested to create an issue at Microsft Connect... Long story short: the key mechanism I didn't clearly understand was DP's value precedence. It is perfectly described here (local value has higher priority than templated parent's value).

Second, not really obvious moment is that the value is considered as templated parent's if it was set by any template (not even element's own template).

HTH.


Maybe you should use TwoWay binding mode? What should you control do with "Some content"? It can not store it in your control's model since binding is OneWay. in your case binding sees that there is a value in your model's property and takes it overwriting "Some content". If don't initialize property, binding does nothing, because it ignores null values and you see "Some content". I hope my explanation is clear.

EDIT

Sorry for little misunderstanding of your problem. I've downloaded your demo app and reproduced the issue. Reading this and this MSDN articles shows that your intentions were right. However you can find there this words:

The following virtual methods or callbacks are potentially called during the computations of the SetValue call that sets a dependency property value: ValidateValueCallback, PropertyChangedCallback, CoerceValueCallback, OnPropertyChanged.

So, setting value of DependencyProperty in constructor potentially is as dangerous as calling a virtual method of object that is not constructed.

Ok, setting a DependencyProperty in constructor is bad. My next idea was to set value in some callback (I've used OnInitialized since it should be called right after Control's constructor). And I found another really strange behavior. If I don't set any value in constructor (this way)

    public CustomControl1()
    {
        //Content = "Initial1";
    }
    protected override void OnInitialized(EventArgs e)
    {
        Content = "Initial2";
        var check = Content; // after this  check == "Initial_2"
    }

I don't see "Initial2" in the window even if I don't specify any value for Content in Window1.xaml. Notice that value is set correctly (as you I see check it). But if I uncomment Content = "Initial1"; string, I see "Initial2". Also if I initialize Content in OnInitialized binding works fine, but it doesn't resolve that actual value of Content is "Initial2". Looks like its source is not that Content property.

I'll continue working around this issue later. I hope this information can be helpful.


Don't initialize the value in ctor, use CoerceValue()

In ctor

public SomeUserControl()
{
    InitializeComponent();
    CoerceValue(SomeProperty);    
}

SomeProperty Defenition

public static readonly DependencyProperty SomeProperty =
    DependencyProperty.Register(
        "Some", typeof(ObservableCollection<IModel>),
        typeof(SomeUserControl),
        new PropertyMetadata()
        {
            DefaultValue = null,
            PropertyChangedCallback = OnSomeChanged,
            CoerceValueCallback = OnCoerceSome
        }
    );

private static object OnCoerceSome(DependencyObject d, object baseValue)
{
    var v = (ObservableCollection<IModel>)baseValue;
    return v ?? new ObservableCollection<IModel>();
}   
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜