Best practices for WPF MVVM UserControls. Avoiding Binding problems - wpf

I am trying to be a good soldier and design some simple User Controls for use in WPF MVVM applications. I am trying (as much as possible) to make the UserControls themselves use MVVM, but I don't think the calling app should know that. The calling app should just be able to slap down the user control, perhaps set one or two properties, and perhaps subscribe to events. Just like when they use a regular control (ComboBox, TextBox, etc.) I'm having a heck of a time getting the bindings right. Notice the use of ElementName in the below View. This is instead of using DataContext. Without further ado, here is my control:
<UserControl x:Class="ControlsLibrary.RecordingListControl"
...
x:Name="parent"
d:DesignHeight="300" d:DesignWidth="300">
<Grid >
<StackPanel Name="LayoutRoot">
<ListBox ItemsSource="{Binding ElementName=parent,Path=Recordings}" Height="100" Margin="5" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=FullDirectoryName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
In the code behind (if there is a way to avoid code behind, please tell me)...
public partial class RecordingListControl : UserControl
{
private RecordingListViewModel vm = new RecordingListViewModel();
public RecordingListControl()
{
InitializeComponent();
// I have tried the next two lines at various times....
// LayoutRoot.DataContext = vm;
//DataContext = vm;
}
public static FrameworkPropertyMetadata md = new FrameworkPropertyMetadata(new PropertyChangedCallback(OnPatientId));
// Dependency property for PatientId
public static readonly DependencyProperty PatientIdProperty =
DependencyProperty.Register("PatientId", typeof(string), typeof(RecordingListControl), md);
public string PatientId
{
get { return (string)GetValue(PatientIdProperty); }
set { SetValue(PatientIdProperty, value);
//vm.SetPatientId(value);
}
}
// this appear to allow us to see if the dependency property is called.
private static void OnPatientId(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RecordingListControl ctrl = (RecordingListControl)d;
string temp = ctrl.PatientId;
}
In my ViewModel I have:
public class RecordingListViewModel : ViewModelBase
{
private ObservableCollection<RecordingInfo> _recordings = null;// = new ObservableCollection<string>();
public RecordingListViewModel()
{
}
public ObservableCollection<RecordingInfo> Recordings
{
get
{
return _recordings;
}
}
public void SetPatientId(string patientId)
{
// bunch of stuff to fill in _recordings....
OnPropertyChanged("Recordings");
}
}
I then put this control down in my main window and like so:
<Grid>
<ctrlLib:RecordingListControl PatientId="{Binding PatientIdMain}" SessionId="{Binding SessionIdMain}" />
<Label Content="{Binding PatientIdMain}" /> // just to show binding is working for non-controls
</Grid>
The error I get when I run all this is:
System.Windows.Data Error: 40 : BindingExpression path error: 'Recordings' property not found on 'object' ''RecordingListControl' (Name='parent')'. BindingExpression:Path=Recordings; DataItem='RecordingListControl' (Name='parent'); target element is 'ListBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
Clearly I have some sort of bindings problem. This is actually much further than I was getting. At least I'm hitting the code in the controls code behind:
OnPatientId.
Before, I didn't have the ElementName in the User Control and was using DataContext and was getting a binding error indicating that PatientIdMain was being considered a member of the user control.
Can someone point me to an example of using a User Control with MVVM design in a MVVM application? I would think this is a fairly common pattern.
Let me know if I can provide more details.
Many thanks,
Dave
Edit 1
I tried har07's idea (see one of the answers). I got:
If I try:
ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"
I get
System.Windows.Data Error: 40 : BindingExpression path error: 'Recordings' property not found on 'object' ''MainViewModel' (HashCode=59109011)'. BindingExpression:Path=DataContext.Recordings; DataItem='RecordingListControl' (Name='parent'); target element is 'ListBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
If I try:
ItemsSource="{Binding Recordings}"
I get
System.Windows.Data Error: 40 : BindingExpression path error: 'Recordings' property not found on 'object' ''MainViewModel' (HashCode=59109011)'. BindingExpression:Path=Recordings; DataItem='MainViewModel' (HashCode=59109011); target element is 'ListBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
I think his first idea (and maybe his second) are very close, but recall, Recordings is defined in the ViewModel, not the view. somehow I need to tell XAML to use viewModel as source. That's what setting the DataContext does, but as I said in the main part, that creates problems elsewhere (you get binding errors related to binding from the MainWindown to properties on the control).
Edit 2. If I try har07's first suggestion:
ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"
AND add in the code behind for the control:
RecordingListViewModel vm = new RecordingListViewModel();
DataContext = vm;
I get:
System.Windows.Data Error: 40 : BindingExpression path error: 'PatientIdMain' property not found on 'object' ''RecordingListViewModel' (HashCode=33515363)'. BindingExpression:Path=PatientIdMain; DataItem='RecordingListViewModel' (HashCode=33515363); target element is 'RecordingListControl' (Name='parent'); target property is 'PatientId' (type 'String')
in other words, the control seems fine, but the binding of the dependency proprerties to the main window seem messed up. The compiler assumes that PatientIdMain is part of RecordingListViewModel.
Various posts indicated that I couldn't set DataContext for this very reason. It would mess up bindings to the main window. See for example:
Binding to a dependency property of a user control WPF/XAML and check out Marc's answer.

You should not set x:Name in a UserControl, since the control has only one Name property, and normally that would be set through the code which uses the control. So, you can't use an ElementName Binding in order to bind to properties of the UserControl itself. Another way to bind to properties of the UserControl inside its content would be to use
{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type my:RecordingsControl}}, Path=Recordings}
In the same way, the client code which uses the control sets its DataContext, either explicitly or through inheritance. Therefore, the instance of the vm you create in the constructor is discarded and replaced by the inherited DataContext as soon as the control is displayed.
You can do two things to solve this: either create the vm outside the UserControl, e.g. as a property of the main window's ViewModel, and use the UserControl like this:
<my:RecordingsControl DataContext="{Binding RecordingListVM}"/>
This way, you wouldn't need any code behind, and the Binding above would simply change to
{Binding Recordings}
Or, create a Recordings property in the UserControl's code behind file, and bind to it as I showed in the first code example.

What you get if binding statement changed this way :
ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"
or this way :
ItemsSource="{Binding Recordings}"
If one of above binding way solve current binding error ("BindingExpression path error: 'Recordings' property not found..."), but lead to another binding error please post the latter error message.
I think the correct binding statement for this part is as mentioned above.
UPDATE :
Responding to your edit. Try to set DataContext locally at StackPanel level, so you can have UserControl set to different DataContext :
public RecordingListControl()
{
InitializeComponent();
RecordingListViewModel vm = new RecordingListViewModel();
LayoutRoot.DataContext = vm;
}
again I can see you've tried this but I think this is the correct way to solve particular binding error ("BindingExpression path error: 'PatientIdMain' property not found..."), so let me know if this solve the error but lead to another binding error.

One possible answer is this (inspired by Dependency property binding usercontrol with a viewmodel)
Simply use DataContext for the UserControl and don't use any of the ElementName business.
I.e.
public partial class RecordingListControl : UserControl
{
public RecordingListViewModel vm = new RecordingListViewModel();
public RecordingListControl()
{
InitializeComponent();
**DataContext = vm;**
}
....
Then, in the MainWindow, you need to bind the user control like so:
<ctrlLib:RecordingListControl
Name="_ctrlRecordings"
PatientId="{Binding RelativeSource={RelativeSource
AncestorType=Window},Path=DataContext.PatientIdMain, Mode=TwoWay}"
SessionId="{Binding RelativeSource={RelativeSource
AncestorType=Window},Path=DataContext.SessionIdMain, Mode=TwoWay}" />
I won't mark this as answer because (besides the audacity of answer one's own question) I don't really like the answer. It forces the application programmer, to remember to put in all this nonsense about AncestorType and RelativeSource. I don't think one has to do this with standard controls, so why here?

Related

DataBinding on Custom Control Not Working

I'm sure I am doing something wrong.
Some context: WPF application, MVVM, Entity Framework, etc.
In my ViewModel for a window, I have two properties: MemberCount and EventCount. Both are int.
In my view, I have a 5 instances of a custom control. This custom control has three dependency Properties: ImageData, ButtonText, and ButtonSubText. All are string.
What I want to do is bind the control in such a way that MemberCount is bound to the ButtonText property on one control, and EventCount is Bound to ButtonText property on another control.
So I do something along the lines of:
<control:MyControl ... ButtonText="{Binding MemberCount, Mode=OneWay}" ButtonSubText="Members" Background="{DynamicResource MetroRed}" />
The DataContext of my window is set to the correct ViewModel.
No matter what I try, I cannot get the member count or event count to show up on the control. I get an error in the output window:
System.Windows.Data Error: 40 : BindingExpression path error: 'MemberCount' property
not found on 'object' ''MyControl' (Name='button')'. BindingExpression:Path=MemberCount;
DataItem='MyControl' (Name='button'); target element is 'MyControl' (Name='button');
target property is 'ButtonText' (type 'String')
I am getting used to DataBinding in WPF, but for the life of me, I can't figure this one out.
What do I need to do to be able to bind a property from my view model to a property on my custom control?
i think the problem is the datacontext or binding within your usercontrol. you should post your usercontrol code.
<MyUserControl x:Name="uc">
<TextBlock Text="{Binding ElementName=uc,Path=ButtonText}/>
</MyUserControl>
check my answer here, there is a similar problem.

Setting DataContext makes PropertyChanged null in UserControl

I have a WPF application which uses a WPF user control.
The user control exposes a DependencyProperty to which I would like to bind to in my WPF application.
As long as my user control does not set its own DataContext this works and I am able to listen to changes in the DependencyProperty.
However the moment I set the DataContext the PropertyChanged being called is null.
What am I missing here?
Code sample:
https://skydrive.live.com/redir.aspx?cid=367c25322257cfda&page=play&resid=367C25322257CFDA!184
DependencyProperty has inheritance property, so if you don't set the UserControlDP's DataContext, the DataContext is inherited from the MainWindow's DataContext. In this case, the UserControlDP's DataContext in your code below is set as MainWindow_ViewModel. Thus, the binding is correctly executed.
<usercontrol:UserControlDP Width="200" Height="100"
TestValue="{Binding TestValueApp, Mode=TwoWay, NotifyOnSourceUpdated=True, NotifyOnTargetUpdated=True}"
Margin="152,54,151,157"></usercontrol:UserControlDP>
In the other case, UserControlDP's DataContext is set as UserControlDP_ViewModel, so the binding is broken. You can see the first exception message as the following at the debug window.
System.Windows.Data Error: 40 : BindingExpression path error: 'TestValueApp' property not found on 'object' ''UserControlDP_ViewModel' (HashCode=24672987)'. BindingExpression:Path=TestValueApp; DataItem='UserControlDP_ViewModel' (HashCode=24672987); target element is 'UserControlDP' (Name=''); target property is 'TestValue' (type 'Object')
Consider setting the DataContext on one of the elements contained within UserControl rather than on UserControl itself.
Thanks for the input and clarifying the details.
After giving it some thought I took the easy way out and removed the ViewModel from the control.
MVVM for the application but no MVVM for the user control.
This way I do not use any bindings in the user control, instead use Dependency Properties which are bound to in the Main application.

Binding a combobox in XAML to a childwindow property

I want to display a child window that contains a combobox with several values coming from one of the child window's property:
public partial class MyChildWindow : ChildWindow
{
private ObservableCollection<MyClass> _collectionToBind = // initialize and add items to collection to make sure it s not empty...
public ObservableCollection<MyClass> CollectionToBind
{
get { return _collectionToBind; }
set { _collectionToBind = value; }
}
}
How do I bind in XAML my combobox to the ComboBoxContent collection (both are in the same class)? I've tried several things such as:
<ComboBox x:Name="linkCombo" ItemsSource="{Binding Path=CollectionToBind }" DisplayMemberPath="Description">
I've only been able to bind it in the code behind file and would like to learn the XAML way to do it.
Thank you!
In this case I would use ElementToElement binding like this:-
<ComboBox x:Name="linkCombo" ItemsSource="{Binding Path=Parent.CollectionToBind, ElementName=LayoutRoot }" DisplayMemberPath="Description">
You give the Content element of the ChildWindow the x:Name of LayoutRoot (in the standard template for child window this is done for you). Hence you can bind to this named element and navigate to the containing ChildWindow by using its Parent property.
Using DataContext = this is tempting and works in simple scenarios but things can get awkward in more complex requirements when the DataContext has already been taken in this way.
You need to set the DataContext of the ChildWindow to what contains the values you'd like to bind to. In this case where you're putting the values you want to bind to on the ChildWindow itself so just put a line in the constructor assigned the DataContext to itself.
DataContext = this;
You can also do this using a RelativeSource binding in the XAML, like this:
{Binding Path=CollectionToBind, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}
However, a better way to do this would be to put the CollectionToBind in a separate class and assign it to the Window DataContext. Now both the Window and the XAML Bindings can all refer to the same class as the DataContext and you can isolate more of your logic into this class rather than putting it in the Window implementation.

How to do simple Binding in Silverlight?

I understand that Silverlight 3.0 has binding but just want a simple example on how to use this to read a property from a class.
I have a class called Appointment which as a String property called Location:
Public Property Location() As String
Get
Return _Location
End Get
Set(ByVal Value As String)
_Location = Value
End Set
End Property
With a Private Declaration for the _Location as String of course.
I want a XAML element to bind to this property to display this in a TextElement, but it must be in XAML and not code, for example I want something like this:
<TextBlock Text="{Binding Appointment.Location}"/>
What do I need to do to get this to work?
It has to be a Silverlight 3.0 solution as some WPF features are not present such as DynamicResource which is what I'm used to using.
Just to add that my XAML is being loaded in from a seperate XAML File, this may be a factor in why the binding examples don't seem to work, as there are different XAML files the same Appointment.Location data needs to be applied.
You have two options.
If the "Appointment" class can be used as the DataContext for the control or Window, you can do:
<TextBlock Text="{Binding Location}" />
If, however, "Appointment" is a property of your current DataContext, you need a more complex path for the binding:
<TextBlock Text="{Binding Path=Appointment.Location}" />
Full details are documented in MSDN under the Binding Declarations page. If neither of these are working, make sure you have the DataContext set correctly.
You need something in code, unless you want to declare an instance of Appointment in a resource and bind to that but I doubt thats what you want.
You need to bind the Text property to the Property Path "Location" then assign the DataContext of the containing XAML to an instance of the Appointment:-
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock Text="{Binding Location}" />
</Grid>
Then in the control's load event:-
void Page_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = new Appointment() { Location = "SomePlace" };
}
Note in this case I'm using the default Page control.
If I'm reading correctly, you need to create an instance of Appointment, set the DataContext of the control to that instance and modify your binding to just say: Text="{Binding Location}"
Also, consider implementing INotifyPropertyChanged on your Appointment class to allow the data classes to notify the UI of property value changes.

WPF decomposition - DataGridTemplateColumn

I am trying to move custom DataGrid column definition into a UserControl.
MyComboBoxColumn.xaml
<dg:DataGridTemplateColumn
x:Class="WpfDecomposition.MyComboBoxColumn"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dg="clr-namespace:Microsoft.Windows.Controls;assembly=WpfToolkit"
x:Name="_this"
>
<dg:DataGridTemplateColumn.Header>
<Button Content="{Binding MyHeader, ElementName=_this}" ></Button>
</dg:DataGridTemplateColumn.Header>
</dg:DataGridTemplateColumn>
MyComboBoxColumn.cs
public partial class MyComboBoxColumn : DataGridTemplateColumn
{
public MyComboBoxColumn()
{
InitializeComponent();
}
public static DependencyProperty MyHeaderProperty =
DependencyProperty.Register("MyHeader", typeof(string), typeof(MyComboBoxColumn), new PropertyMetadata("TEST"));
}
Main windows XAML:
<dg:DataGrid CanUserAddRows="True" AutoGenerateColumns="False">
<dg:DataGrid.Columns>
<my:MyComboBoxColumn />
</dg:DataGrid.Columns>
</dg:DataGrid>
I would expect to see a button "TEST" in the column's header, but instead I see the empty button. Looks like the binding is broken. What is wrong?
It's not working because it can't find an element with the name _this. I get the following error in the Output window when I debug your code in Visual Studio:
System.Windows.Data Error: 4 : Cannot
find source for binding with reference
'ElementName=_this'.
BindingExpression:Path=MyHeader;
DataItem=null; target element is
'Button' (Name='TestButton'); target
property is 'Content' (type 'Object')
As for why it can't find it - I think that is because WPF bindings use the visual tree to find the source of the binding. In this case, the MyComboBoxColumn is not in the visual tree, so therefore it can't find an element with that name.
I also tried using RelativeSource to find the element, but that didn't work either - likely for the same reason.
The only thing that I could get to work is to set the DataContext of the button to the column itself in the constructor:
public MyComboBoxColumn()
{
InitializeComponent();
this.TestButton.DataContext = this;
}
And then change the binding in the XAML:
<tk:DataGridTemplateColumn.Header>
<Button Content="{Binding Path=MyHeader}" x:Name="TestButton" />
</tk:DataGridTemplateColumn.Header>
That doesn't seem like the best way to do it, but at least it works.
If you don't want to or can't set the DataContext in the constructor (e.g. when creting columns dynamically in the code), set the column's Header property to the object you want to bind to (the data context) and then you can bind to this object in the HeaderStyle data template.
See this question for details.

Resources