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.
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.
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
Obviously the Views are the Controls wrapped by your MainWindow.
Hope it's clear.
Lets answer your questions in sequence:
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.
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.
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.
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.
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.
Related
I am looking at a problem where my own created gauge control doesn't deal with bindings correctly during startup/creation. It works fine once user control and viewmodel are instantiated and all bindings are set.
I have the following control (all user code):
<linearGauge:LinearGaugeControl
Grid.Row="2"
Margin="30, 0, 0, 0"
GaugeLabel="Flow"
LinearGaugeLength="800"
LinearGaugeHeight="80"
LabelFontSize="20"
NeedleColor="Black"
Grid.Column="0" Grid.ColumnSpan="2"
DataContext="{Binding FlowGaugeData}" />
This question is about two properties in the xaml above:
LinearGaugeLength="800"
LinearGaugeHeight="80"
These properties are bound to the view model of the LinearGaugeControl :
<UserControl.Resources>
<Style TargetType="local:LinearGaugeControl">
<Setter Property="LinearGaugeLength" Value="{Binding GaugeSize, Mode=OneWayToSource}"/>
<Setter Property="LinearGaugeHeight" Value="{Binding BarThickness, Mode=OneWayToSource, NotifyOnSourceUpdated=True}"/>
</Style>
</UserControl.Resources>
From my previous struggle with DPs I learned something useful from user ASh, that I can add a callback to my DP so that I at least know that it gets triggered. That works fine. The callback of the LinearGaugeHeight DP fires with the 'new' value of 80 (old value=50).
After that event, my setter gets called, with the wrong value ! (50, the default value of the DP).
What is going wrong?
Is this going wrong because the view is created before the viewmodel is?
In reply to #Clemens:
We are using caliburn, can that in some way cause this problem? I think I read somewhere that caliburn uses DataContext=this under the hoods?
I've created a custom control that is styled and configures in its own XAML sheet. Databindings in this control uses a specific object (CProject class).
Just to clarify, the control is a project frame, that has controls for settings and a canvas that will be the workspace for each/any project.
The project control (IPProjectPanel) inherits "Frame", and also adds a "settings" stack panel to its children list which in turn contains controls for - well, settings.
The CProject class however, is the pure functional part, with no UI interaction or handling whatsoever. So, I need to "plug" an instance of CProject into every unique project that can be active. So, I want to set a specific instance of CProject as datacontext to every IPProjectPanel instance in a tabpanel. Either I want to set the datacontext by code, or have it created by settings datacontext in XAML, and retrieving it after it has been initialized.
The problem though, is that I cant quite figure out either.
Here is a snippet of the style of the IPProjectPanel in XAML, that uses the approach to set datacontext in XAML:
<Style TargetType="{x:Type ip:IPProjectGrid}">
<Setter Property="OverridesDefaultStyle"
Value="True" />
<Setter Property="SnapsToDevicePixels"
Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ip:IPProjectGrid}">
<Grid Background="White"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="0">
<!---->
<Grid.DataContext>
<ipp:CProject></ipp:CProject>
</Grid.DataContext>
<StackPanel x:Name="PART_settingsPanel"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
MinWidth="300" Background="Gray">
<GroupBox Header="Project settings">
<StackPanel>
....
</style>
Here it is set as a context to Grid, but I'd like to have it as a context of the actual class (IPProjectPanel).
So, the IPProjectPanel instance is created by code (for now..), and I need to retrieve the CProject instance (or set one) so that I can work with it.
I'd like to keep to C#/WPF ways to do stuff, as this app is also training for WPF and C# concepts and such. So the "best C#-WPF" way to do it, is very welcome, but a solution either way!
Thank you for your time.
So in general, the datacontext is primary inteded to be for your ViewModel, and in fact WPF is really set up for doing MVVM (Model View ViewModel) style applications. It's actually fairly simple to learn, but if you're looking for the "Best C#-WPF" way of doing things, take the time to learn MVVM. It's really fairly straightforward.
Simple Example from CodeProject:
http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial
From Microsoft (somewhat heavy reading):
http://msdn.microsoft.com/en-us/library/gg405484(v=pandp.40).aspx
The way you have this set up currently has the potential to create some nasty bugs. You should never declare a DataContext object instance inside a template unless you never plan on accessing it outside of that template's scope. By doing so you will be creating a new instance of the CProject class any time the control needs to be visually re-loaded (like changing tabs) and you may end up referencing an old CProject instance in code while displaying a completely separate one on the screen. Declaring a DataContext object not in a template (i.e. Window.DataContext) is fine.
If you want each control instance to create its own CProject instance you would be better off doing that in code in the constructor and exposing that as a property on the control which you can then bind your Grid.DataContext to inside the template. Avoid setting it to the DataContext property of the control itself as this will cause any implicit source Bindings that are set on the control where it is declared in XAML to break by overriding the inherited DataContext:
Grid.DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=PropertyWithCProject}"
It's probably more likely that you will want to control the CProject instances externally and hand them to the control instances. To do this you can create them in a container ViewModel class (MVVM pattern) and set this as a DataContext higher up on something that will contain all of your custom controls. You can then expose individual CProjects or a collection of them and bind your controls' DataContexts to those.
We have a WPF application with a standard MVVM pattern, leveraging Cinch (and therefore MefedMVVM) for View -> ViewModel resolution. This works well, and I can bind the relevant controls to properties on the ViewModel.
Within a particular View, we have an Infragistics XamGrid. This grid is bound to an ObservableCollection on the ViewModel, and displays the appropriate rows. However, I then have a specific column on this grid which I am trying to bind a TextBox text value to a property on the parent DataContext, rather than the ObservableCollection. This binding is failing.
We've gone through several options here including:
Using AncestorType to track up the tree and bind to the DataContext of the parent UserControl like so (from the great answer to this question, as well as this one)...
{Binding Path=PathToProperty, RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
Specifying the ElementName and trying to target the top level control directly. Have a look here if you'd like to read about using ElementName.
Using a 'proxy' FrameorkElement defined in the resources for the UserControl to try and 'pass in' the context as required. We define the element as below, then reference as a static resource...
<FrameworkElement x:Key="ProxyContext" DataContext="{Binding Path=DataContext, RelativeSource={RelativeSource Self}}"></FrameworkElement>
In this case the binding finds the FrameworkElement, but can not access anything beyond that (when specifying a Path).
Having read around, it looks quite likely that this is caused by the Infragistics XamGrid building columns outside of the tree. However, even if this is the case, at least options 2 or 3 should work.
Our last thoughts are that it is related to the V - VM binding, but even using Snoop we've yet to find what the exact issue is. I'm by no means an expert with WPF binding so any pointers would be appreciated.
EDIT: I have found some templating examples from Infragistics here that I will try.
EDIT 2: As pointed out by #Dtex, templates are the way to go. Here is the relevant snippet for use with a XamGrid:
<ig:GroupColumn Key="CurrentDate">
<ig:GroupColumn.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=DataContext.CurrentDateTest, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</DataTemplate>
</ig:GroupColumn.HeaderTemplate>
<ig:GroupColumn.Columns>
I've left the XML open... you'd simply add the columns you wanted, then close off the relevant tags.
I dont know about XamGrid but that's what i'll do with a standard wpf DataGrid:
<DataGrid>
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataContext.MyProperty, RelativeSource={RelativeSource AncestorType=MyUserControl}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding DataContext.MyProperty, RelativeSource={RelativeSource AncestorType=MyUserControl}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Since the TextBlock and the TextBox specified in the cell templates will be part of the visual tree, you can walk up and find whatever control you need.
Because of things like this, as a general rule of thumb, I try to avoid as much XAML "trickery" as possible and keep the XAML as dumb and simple as possible and do the rest in the ViewModel (or attached properties or IValueConverters etc. if really necessary).
If possible I would give the ViewModel of the current DataContext a reference (i.e. property) to the relevant parent ViewModel
public class ThisViewModel : ViewModelBase
{
TypeOfAncestorViewModel Parent { get; set; }
}
and bind against that directly instead.
<TextBox Text="{Binding Parent}" />
How to focus a textbox from ViewModel wpf?
<TextBox Name="PropertySearch"
Text="{Binding UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay, Path=PropertySearch,
ValidatesOnDataErrors=True}"
Width="110"
Height="25"
Margin="10" />
You can do this by adding a property to your ViewModel (or use an existing property) that indicates when the SetFocus should happen but the View should be responsible for actually setting the focus since that is purely View related.
You can do this with a DataTrigger.
View:
<Grid Name="LayoutRoot" DataContext="{StaticResource MyViewModelInstance}">
<Grid.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding UserShouldEditValueNow}" Value="True">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=PropertySearch}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<TextBox Name="PropertySearch" Text="{Binding UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Path=PropertySearch, ValidatesOnDataErrors=True}" Width="110" Height="25" Margin="10" />
</Grid>
ViewModel:
// When you think the view should set focus on a control
this.UserShouldEditValueNow = true;
The example above is simplified by just using a boolean ViewModel property "UserShouldEditValueNow". You can add a property like this to your ViewModel or use some other exising property that indicates this state.
Note: So why is it done this way in MVVM? One reason is, suppose the View author decided to replace the TextBox with a ComboBox, or even better, suppose your property was an integer value that had both a TextBox to view/edit the number and a Slider as another way to edit the same value, both controls bound to the same property... how would the ViewModel know which control to set focus on? (when it shouldn't even know what control, or controls, are bound to it in the first place) This way the View can select which control to focus by changing the ElementName binding target in the DataTrigger Setter.
Happy coding!
The question you should be asking yourself is "why does my ViewModel need to know which control has the focus?"
I'd argue for focus being a view-only property; it's an interaction property, and has nothing to do with the conceptual state. This is akin to the background color of a control: why would you represent it in the VM? If you need to manage the focus in a custom way, it's probably better to use a view-level object to do the job.
In your parent control, add the following property:
FocusManager.FocusedElement="{Binding ElementName=PropertySearch}"
While purists may argue for leaving this out of the VM, there are cases where it may make sense to do so from the VM.
My approach has been to make the view implement an interface, pass that interface to the ViewModel, and then let the VM call methods on the interface.
Example:
public interface IFocusContainer
{
void SetFocus(string target);
}
A couple things to keep in mind:
A VM might serve more than one instance of a view, so your VM might want to have a collection of references to IFocusContainer instances, not just one.
Code the VM defensively. You don't know whether there are 0, 1 or 20 views listening.
The "target" parameter of SetFocus() should probably be "loosely" coupled to the VM. You don't want the VM caring about the exact control names in the UI. Rather, the VM should indicate a name that is defined solely for focus management. In my case, I created some attached properties that would allow me to "tag" controls with "focus names".
To implement the interface, you can:
Implement it in the code-behind
Create some behaviors that know how to attach to the ViewModel that is present in the DataContext.
There's nothing wrong with implementing it on the Code Behind, but the behavior approach does allow a XAML only hookup if that's important to you.
In the implementation of the interface, you can use the visual tree to locate the control, or you could just code up a switch statement for a known set of focusable items.
I have some code that looks like this:
<Expander Header="{Binding SelectedSlot.Name}"
Visibility="{Binding ShowGroupSlot, Converter={StaticResource BooleanToVisibility}}">
<Controls:GroupPrototypeSlotControl Slot="{Binding DataContext.SelectedSlot,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Expander}}}" />
</Expander>
This works, but the ugliness of the Slot Binding bothers me. This is required because the GroupPrototypeSlotControl has a GroupPrototypeViewModel as its DataContext. If I simply use {Binding SelectedSlot}, it attempts to resolve it on the 'child' ViewModel, which fails. I get around this by explicitly looking at the DataContext of my parent control. Is there a cleaner way to do this type of binding?
EDIT: I found a cleaner way of resolving my problem, though it still feels like a hack. I modified the GroupPrototypeSlotControl so that it has a top-level LayoutRoot (a StackPanel, in this case) and then set the DataContext of LayoutRoot to the ViewModel rather than setting the DataContext of the entire control. This allows me to use the {Binding SelectedSlot} syntax where I use the control (since the control still has the parent DataContext), at the cost of slightly increasing the complexity of the control. In general, this is probably the better pattern for a custom control, since the consumer of the control expects a {Binding} to resolve to their parent DataContext if one isn't explicitly specified.
A slightly cleaner (shorter) way is to use ElementName in your Binding like this:
<Expander Header="{Binding SelectedSlot.Name}"
x:Name="expander"
Visibility="{Binding ShowGroupSlot, Converter={StaticResource BooleanToVisibility}}">
<Controls:GroupPrototypeSlotControl Slot="{Binding DataContext.SelectedSlot, ElementName=expander}" />
</Expander>