Search code examples
wpfdata-bindingmvvmuser-controlswpf-controls

Best practices for WPF MVVM UserControls. Avoiding Binding problems


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.


Solution

  • 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.