开发者

Debugging: Why a data-bound section breaks off when DataContext is reapplied?

Update 2010 06 12

Distilled the essence out into a smaller sample - solution available here http://db.tt/v6r45a4

It now seems to be a problem related to the tab control : When the data context is reapplied (click the button), it seems that everything works as expected. The bindings refresh but then all bindings on the active tab are broken.

You can verify this by selecting any tab and clicking the button, you'd see the controls on that tab go kaput. Added a bunch of logging statements which is how my friend arrived at this synopsis and his fix [ Set DataContext="{Binding}" on the tab control as well ]

But we're still not sure why this behaves the way it does...

TabControl Data Context now set to ReproTabItemBug.MainViewModel
TabPage [LeftTabPage] Data Context now set to ReproTabItemBug.LeftViewModel
System.Windows.Data Error: 40 : BindingExpression path error: 'MiddleProp' property not found on 'object' ''MainViewModel' (HashCode=50608417)'. BindingExpression:Path=MiddleProp; DataItem='MainViewModel' (HashCode=50608417); target element is 'TextBox' (Name='MiddleTabTextbox'); target property is 'Text' (type 'String')
TabPage [MiddleTabPage] Data Context now set to ReproTabItemBug.MiddleViewModel
Middle tab textbox text changed to 634272638824920423
TabPage [RightTabPage] Data Context now set to ReproTabItemBug.RightViewModel
Middle tab textbox text changed to 

Previous Post (Disclaimer: Long post ahead... get your popcorn. I've spent the good part of a day on this..)

My ViewModel is composed of three POCO subViewModels. Each subVieModel has properties that are bound in the 3 sections mentioned above. Each subViewModel is exposed as a standard .net get-only property (No INotifyPropertyChanged)

My View has 3 sections. Each section has a DataContext setter something like this..

...
<TabItem  x:Name="_tabPageForVM2"  DataContext="{Binding PropReturningSubVM2}">
  <!-- followed by UI Items that are data-bound to props inside this sub-viewmodel -->
</TabItem>
...

So to summarize

<MainView> <!--DataContext set programmmatically to an Instance Of MainViewModel) -->
   <Control DataContext="{Binding PropReturningSubVM1}" >.. Section1 .. </Control>
   <Control DataContext="{Binding PropReturningSubVM2}" >.. Section2 .. </Control>
   <Control DataContext="{Binding PropReturningSubVM3}" >.. Section3 .. </Control>
</MainView>

Now here's the puzzling bit. On normal startup, I create an instance of the MainViewModel (which has the child view models passed into its ctor). The properties in a watch window confirm this.

    Trace.WriteLine("Before setting datacontext");
    mainView.DataContext = null;
    mainView.DataContext = mainViewModel;
    //mainView.Refresh();
    Trace.WriteLine("After setting datacontext");

Everything works perfectly. Now due to reasons beyond my control, there is a scenario where the UI is dismissed but the view still resides in memory. So to clear it out when it is shown the next time, I create new instances of my viewmodel and reapply the datacontext (by calling the same initialization routine as before) However when the DataContext=mainViewModel set is executed, I see a bunch of binding errors in the output window. What is interesting is that only the bindings inside one tab page (a sub view model) are broken. The other 2 sub view models function correctly - no binding errors.

System.Windows.Data Error: 40 : BindingExpression path error: 'RphGaugeMaxScale' property not found on 'object' ''MainViewModel' (HashCode=38546056)'. BindingExpression:Path=RphGaugeMaxScale; DataItem='MainViewModel' (HashCode=38546056); target element is 'Gauge' (Name='GaugeControl'); target property is 'MaxValue' (type 'Double')
System.Windows.Data Error: 40 : BindingExpression path error: 'RphGaugeMaxScale' property not found on 'object' ''MainViewModel' (HashCode=38546056)'. BindingExpression:Path=RphGaugeMaxScale; DataItem='MainViewModel' (HashCode=38546056); target element is 'Gauge' (Name='GaugeControl'); target property is 'MajorTickCount' (type 'Int32')
...

The properties exist on SubViewModel2, but instead the lookup is using the MainViewModel.

Next I added a Refresh Method on MainView (called after the DataContext is reapplied) that does this

    _tabPageForVM2.DataContext = null;
    _tabPageForVM2.DataContext = mainVM.PropReturningSubVM2;

and this fixes the problem.

What puzzles me is

  • what is special about subVM2 ? If it works the first time DataContext is assigned, why does it break the second time around ?
  • I set the header of the tabPage to {Binding} to check what it is bound to and it showns "FullTypeNameOfSubVM2", which indicates the DataContext property of the tab page is being set. 开发者_开发知识库So why are the bindings broken?


You can download Snoop or other tool that shows you visual tree and see there that content of TabItem is not actually the visual child of this TabItem, but the visual child of TabControl.

Debugging: Why a data-bound section breaks off when DataContext is reapplied?

So TabItem is the logical child of TabItem's content and TabControl is the visual child of TabItem's content. DataContext should be inherited from logical parent, but it seems to me that it's inherited randomly from either TabItem or TabControl.

The best solution I can suggest is to move bindings to content like so:

<TabItem x:Name="LeftTabPage" Header="LeftModel">
    <StackPanel Orientation="Horizontal" DataContext="{Binding Left}">
        <my:Gauge x:Name="gauge" Height="200" Width="200" Value="{Binding LeftProp}"/>
        <Viewbox Height="200" Width="200" >
            <my:Gauge x:Name="scaledGauge" Value="{Binding LeftProp}"/>
        </Viewbox>
    </StackPanel>
</TabItem>

<TabItem x:Name="MiddleTabPage" Header="{Binding}">
    <TextBox x:Name="MiddleTabTextbox" DataContext="{Binding Middle}" Text="{Binding MiddleProp}" />
</TabItem>

<TabItem x:Name="RightTabPage" Header="RightModel">
    <TextBox DataContext="{Binding Right}" Text="{Binding RightProp}"/>
</TabItem>

I think I get it. I'll try to explain.

If the second tab is active(for example), MiddleTabTextbox is logical child of MiddleTabPage and it is visual child of MyTabControl (it has 2 different parents with 2 different DataContexts). First tab is not active and it has only logical child - StackPanel. When you click the button DataContext changes. And it's ok with StackPanel - it has one parent. But what MiddleTabTextbox should do? Shoult it take DataContext from visual or logical parent? It seems logical to get context from logical parent :) I expeted this behaviour, but MiddleTabTextbox gets it from visual child i.e. MyTabControl. So instead getting MiddleViewModel you get MainViewModel with bunch of binding errors. I don't know why WPF inherit DataContext from visual parent not logical one.


This:

DataContext="{Binding PropReturningSubVM2}"

looks suspicious. You're using a Binding expression, and not specifying a Source or RelativeSource, which means you want it to look for a property called PropReturningSubVM2 on whatever object your DataContext references, and bind the result of that expression back to your DataContext.

So you've got a sort of circular-reference thing going on here; you're declaratively saying that your DataContext is this other thing on your DataContext. In procedural code, that would be fine, since you're in full control of when it happens, you do it, and you move on. But XAML isn't procedural, it's declarative; so you're declaring, "This is the relationship that I want to set up and enforce", even though the relationship you're declaring is self-contradictory. If it works at all, it's only because the framework is doing things in a specific order that happens to make it work (first the property is inherited from the parent control, then bindings are applied, and the property change caused by the binding doesn't trigger the binding to run again). Apparently sometimes things don't happen in that specific order, hence your problem.

I'd suggest removing the circular reference. Change your binding to something like this:

DataContext="{Binding PropReturningSubVM2,
 RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}}"

This removes the weird circular-reference thing, because you're no longer binding your DataContext based on your own DataContext, you're now explicitly binding it based on your parent TabControl's DataContext -- so no more circularity, no more ordering dependency, and hopefully no more bug.

(Substitute a different type instead of TabControl if appropriate -- ideally you'd probably want to use the type of the parent control that you're programmatically setting the DataContext on to begin with.)


All the items controls automatically set the DataContext on their items as far as I know. The binding on your local datacontext should take care of that behaviour. What happens as I can see is when the bindings reevaluate the local DataContext is empty and the bindings resolve to the DataContext a level above.
I would try setting the updatebehaviours on the binding by setting BindingMode. If that doesn't work I would try to force update at the right time. One way to do that is by using a multibinding: 1. binding in the multibinding is your binding as now 2. binding is a binding to the parent datacontext

the multivalueconverter just passes the first value on bothways - this way you get your binding to update with any change you want - 2. binding is only for the extra update when parent datacontext (or another value changes).

0

上一篇:

下一篇:

精彩评论

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

最新问答

问答排行榜