7

First things first, some context. If you're familiar with the problem, skip down to the BindingExpression part. This is my first major project in WPF, so I am still quite new to the MVVM pattern. Here is the only other similar question I have found, whose lacklustre answer doesn't really enthuse me much.

I have/am building a .NET 3.5 WPF application and I am using MVVM (implemented myself, no framework). Within this, I have a number of Views and ViewModels. These reside within a master ApplicationView and ApplicationViewModel respectively.

The way I change views is through using XAML DataTemplate elements in the ApplicationView, like so:

<DataTemplate DataType="{x:Type viewmodels:InitViewModel}">
    <views:InitView />
</DataTemplate>

And then in the main body I have a ContentControl which binds to a property in ApplicationViewModel

<ContentControl Content="{Binding CurrentPageViewModel}"/>

When I run the application, all of this appears to work fine, and does exactly what is intended. However, when I look at the Debug output after the run, I get a lot of BindingExpression errors.

Here is one for example. I have a property, SplashText, in my InitViewModel. This is bound to a textblock in the splash screen (InitView). When the splash screen ends and I switch out the viewmodel, I get the following:

System.Windows.Data Error: 39 : BindingExpression path error: 'SplashText' property not found on 'object' ''MainMenuViewModel' (HashCode=680171)'. BindingExpression:Path=SplashText; DataItem='MainMenuViewModel' (HashCode=680171); target element is 'TextBox' (Name='FeedBackBox'); target property is 'Text' (type 'String')

I understand that this is because the bindings still exist, but the CurrentPageViewModel property of the DataContext has changed. So what I want to know is:

  • Is this a fleeting problem, i.e. are the views disposed of when not being used or do they (and the bad bindings) sit there in memory indefinitely?
  • Is there a way I can clean up or deactivate these bindings while the view is inactive?
  • What sort of performance knock is it going to have on my application if I leave these alone?
  • Is there a better way of switching views which avoids this problem?

Thanks in advance, and apologies for the monolithic question.

Edit 03/09/13 - Thanks to Jehof, Francesco De Lisi and Faster Solutions for pointing out that it is pointless to set sub-views datacontext as {Binding DataContext.CurrentPageViewModel, RelativeSource={RelativeSource AncestorType={x:Type Window}}} because the ContentControl takes care of the datacontext.

Community
  • 1
  • 1
Will Faithfull
  • 1,868
  • 13
  • 20

4 Answers4

3

Your specific example is not reproducible in .NET 4.5, which probably means Microsoft has fixed the problem meantime.

Nevertheless, a similar problem exists when Content and ContentTemplate are both data-bound. I am going to address that problem, which is also likely to solve problems in .NET 3.5 if anyone is still using it. For example:

<ContentControl Content="{Binding Content}" ContentTemplate="{Binding Template}" />

Or when ContentTemplate is determined by DataTrigger:

<ContentControl Content="{Binding Content}">
    <ContentControl.Style>
        <Style TargetType="{x:Type ContentControl}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding Choice}" Value="1">
                    <Setter Property="ContentTemplate" Value="{StaticResource TemplateA}" />
                </DataTrigger>
                <DataTrigger Binding="{Binding Choice}" Value="2">
                    <Setter Property="ContentTemplate" Value="{StaticResource TemplateB}" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </ContentControl.Style>
</ContentControl>

In both cases, one gets binding errors similar to those OP observed.

The trick here is to ensure that changes to Content and ContentTemplate are performed in just the right order to prevent binding errors. I've written DelayedContentControl, which ensures that Content and ContentTemplate are changed at the same time and in the right order.

<jc:DelayedContentControl Content="{Binding Content}" ContentTemplate="{Binding Template}">

Similarly for the DataTrigger case.

You can get DelayedContentControl from my opensource JungleControls library.

Robert Važan
  • 3,399
  • 2
  • 25
  • 31
  • I know this is quite old, but I just came across the same problem, and tried to use this solution. While the DelayedContentControl does what it is coded for, it is actually really slow (I've this bindingexpression problem inside a TabControl, so the simple act of "changing tab" takes a couple of seconds) Have you ever come across any other possible solution that could help in this scenario? – Easly Apr 27 '20 at 06:23
1

It looks like your DataContext goes to MainMenuViewModel while your property belongs to another ViewModel generating the error.

The CurrentPageViewModel value before and after the splash screen changes losing its Binding while switching view.

The problem is dued to DataContext="{Binding DataContext.CurrentPageViewModel, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"

In fact, CurrentPageViewModel = InitViewModel when your application starts, but the problem is that every View has the same DataContext (i.e. InitViewModel at first) but I'm sure the ViewModels haven't the entire pool of needed properties to satisfy view bindings. An example to understand:

ViewX has a binding to PropertyX, managed in ViewModelX. ViewY has a binding to PropertyY, managed in ViewModelY. Both have DataContext = CurrentViewModel.

On the startup CurrentViewModel = ViewModelX and both ViewX and ViewY have DataContext = ViewModelX. But this is wrong! And probably will generate an error.

What I usually do is to set in the View class the DataContext (cs or XAML if you prefer) with the corresponding View Model to be sure it fits. Then, when needed, I call a refresh method to update my values every time I switch page. If you have shared properties consider to use a Model to centralize your informations (and values).

A sample image from http://wildermuth.com/images/mvvm_layout.png

enter image description here

Obviously the Views are the Controls wrapped by your MainWindow.

Hope it's clear.

Francesco De Lisi
  • 1,493
  • 8
  • 20
  • So what you are suggesting is to hard-code the datacontext into each view directly rather than binding to a currentViewModel property in the master view model? To me, that seems a bit messy. However, I think this answer is probably the closest of the 3 so far to actually offering a solution. – Will Faithfull Sep 03 '13 at 13:27
  • 1:1 Binding is the simplest solution. Take a look at http://msdn.microsoft.com/en-us/library/gg405494(v=pandp.40).aspx for advanced MVVM pattern. – Francesco De Lisi Sep 03 '13 at 13:53
  • by 1:1 binding, do you mean Mode=OneTime? – Will Faithfull Sep 03 '13 at 14:00
0

Lets answer your questions in sequence:

  1. You probably already know the answer to this. When .Net garbage collects it'll remove your View object from the heap. But until this time your View object is still bound to the main DataContext on your page and will react to DataContext changed events.
  2. The obvious thing to do is to set the Views DataContext to null. DataContext is a dependency property so the null values scope will just be your View.
  3. As the other/lackluster answer said, it'll slow you down a bit but not a lot. I wouldn't worry too much about this.
  4. Yes. Here's a useful thread on view navigation options: View Navigation Options

I'd also suggest looking at a framework. Something light-weight like MVVM Light will solve a bunch of problems for you with very little integration. It's ViewModelLocator pattern also does what you're doing, but without the side-effects and provides a whole bunch of cleanup options.

Faster Solutions
  • 7,005
  • 3
  • 29
  • 45
  • But would setting the datacontext to null when the view is no longer in scope stop the bindingexpression path errors? And how would you decide when to nullify the datacontext, on a particular event of the usercontrol? – Will Faithfull Sep 03 '13 at 13:29
  • I think it would. You see, DataContext is a Dependency Property. Because it's a Dependency Property it will do a couple of things: It will notify bindings of changes and it will hold a hierarchy of values. By having its own value it will no longer depend on the value it inherits from your Page/User Control/Window. As for deciding when to nullify the context, that depends on your apps structure. One option would be to monitor the DataContextChanged event on your child View. This would tell you when the DataContext had changed and you could then react to that event. – Faster Solutions Sep 03 '13 at 15:02
  • That's all well and good, and would fix the transition from `InitView` to `MainMenuView` because we never have to re-visit the splash screen. However, the problem comes when for example, I switch to a workflow viewmodel off the main menu, and then later go back to the main menu. It's datacontext would then be null, and I would need to find a way to reset it, or just accept the binding expression errors. – Will Faithfull Sep 03 '13 at 16:14
  • Right, so you've got a collection of singleton views and viewmodels sharing a single DataContext binding. This means that, if you're holding references to each of your views you're going to get binding expression errors from every view that has been opened but is no longer visible - so the number of binding expression errors is going to increase for the user as they navigate through the application. Your best option is to use a page-based navigation system (google "page based navigation wpf"). WPF provides all the scaffolding you need and that will resolve this. – Faster Solutions Sep 03 '13 at 16:39
  • This is awfully wrong. When ContentTemplate changes, all previously generated controls are _destroyed_ _immediately_. Bindings stop working shortly before that. GC doesn't play any role here. – Robert Važan Jul 18 '14 at 00:48
0

You can omit the binding of the DataContext in your Views

DataContext="{Binding DataContext.CurrentPageViewModel, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"

cause the DataContext of your View is the DataContext of the ContentControl and that gets set by your binding of the Content-Property.

So, when your property CurrentPageViewModel is set to an InitViewModel the ContentControl will use the InitViewModel as DataContext and use the InitView as ContentTemplate and it will set his own DataContext as DataContext of the InitView.

Jehof
  • 34,674
  • 10
  • 123
  • 155