Binding nested data using MVVM in WPF not working

I cannot understand why my third nested DataBinding in WPF does not work. I use Entity Framework and Sql Server 2012, and the following are my entities. An application can have multiple accounts. There is an account table and an application table.

ENTITIES
1. Applications
2. Accounts

ViewModels
1. ApplicationListViewModel
2. ApplicationViewModel
3. AccountListViewModel
4. AccountViewModel

In my usercontrol, I am trying to do the following:
1. Using combobox, select an application using ApplicationListViewModel ( Work )
2. In the selected application, display all accounts in the datagrid ( Work )
3. With the selected account, information about the specific account is displayed. ( Does not display information about the selected account )

<UserControl.Resources> <vm:ApplicationListViewModel x:Key="AppList" /> </UserControl.Resources> <StackPanel DataContext="{Binding Source={StaticResource AppList}}"> <Grid> <Grid.RowDefinitions> ... </Grid.ColumnDefinitions> <StackPanel Grid.Row="0" Grid.Column="0"> <GroupBox Header="View all"> <StackPanel> <!-- All Applications List --> <ComboBox x:Name="cbxApplicationList" ItemsSource="{Binding Path=ApplicationList}" DisplayMemberPath="Title" SelectedValuePath="Id" SelectedItem="{Binding Path=SelectedApplication, Mode=TwoWay}" IsSynchronizedWithCurrentItem="True" /> <!-- Selected Application Accounts --> <DataGrid x:Name="dtgAccounts" Height="Auto" Width="auto" AutoGenerateColumns="False" DataContext="{Binding SelectedApplication.AccountLVM}" ItemsSource="{Binding Path=AccountList}" SelectedItem="{Binding SelectedAccount, Mode=TwoWay}" IsSynchronizedWithCurrentItem="True"> <DataGrid.Columns> <DataGridTextColumn Header="Title" Binding="{Binding Path=Title}"></DataGridTextColumn> </DataGrid.Columns> </DataGrid> </StackPanel> </GroupBox> </StackPanel> <StackPanel Grid.Row="0" Grid.Column="1" > <GroupBox x:Name="grpBoxAccountDetails" Header="New Account" > <!-- Selected Account Details --> <!-- DataContext binding does not appear to work --> <StackPanel DataContext="{Binding SelectedApplication.AccountLVM.SelectedAccount}" > <Grid> <Grid.RowDefinitions> ... </Grid.ColumnDefinitions> <TextBlock x:Name="lblApplication" Grid.Row="0" Grid.Column="0" >Application</TextBlock> <ComboBox x:Name="cbxApplication" Grid.Row="0" Grid.Column="1" DataContext="{Binding Source={StaticResource AppList}}" ItemsSource="{Binding ApplicationList}" DisplayMemberPath="Title" SelectedValuePath="Id" SelectedValue="{Binding SelectedApplication.AccountLVM.SelectedAccount.ApplicationId}"> </ComboBox> <TextBlock x:Name="lblTitle" Grid.Row="0" Grid.Column="0" >Title</TextBlock> <TextBox x:Name="txtTitle" Grid.Row="0" Grid.Column="1" Height="30" Width="200" Text="{Binding Title}" DataContext="{Binding Mode=OneWay}"></TextBox> <Button Grid.Row="1" Grid.Column="0" Command="{Binding AddAccount}">Add</Button> </Grid> </StackPanel> </GroupBox> </StackPanel> </Grid> </StackPanel> 

ApplicationListViewModel

 class ApplicationListViewModel : ViewModelBase { myEntities context = new myEntities(); private static ApplicationListViewModel instance = null; private ObservableCollection<ApplicationViewModel> _ApplicationList = null; public ObservableCollection<ApplicationViewModel> ApplicationList { get { return GetApplications(); } set { _ApplicationList = value; OnPropertyChanged("ApplicationList"); } } //public ObservableCollection<ApplicationViewModel> Cu private ApplicationViewModel selectedApplication = null; public ApplicationViewModel SelectedApplication { get { return selectedApplication; } set { selectedApplication = value; OnPropertyChanged("SelectedApplication"); } } //private ICommand showAddCommand; public ApplicationListViewModel() { this._ApplicationList = GetApplications(); } internal ObservableCollection<ApplicationViewModel> GetApplications() { if (_ApplicationList == null) _ApplicationList = new ObservableCollection<ApplicationViewModel>(); _ApplicationList.Clear(); foreach (Application item in context.Applications) { ApplicationViewModel a = new ApplicationViewModel(item); _ApplicationList.Add(a); } return _ApplicationList; } public static ApplicationListViewModel Instance() { if (instance == null) instance = new ApplicationListViewModel(); return instance; } } 

ApplicationViewModel

 class ApplicationViewModel : ViewModelBase { private myEntities context = new myEntities(); private ApplicationViewModel originalValue; public ApplicationViewModel() { } public ApplicationViewModel(Application acc) { //Initialize property values this.originalValue = (ApplicationViewModel)this.MemberwiseClone(); } public ApplicationListViewModel Container { get { return ApplicationListViewModel.Instance(); } } private AccountListViewModel _AccountLVM = null; public AccountListViewModel AccountLVM { get { return GetAccounts(); } set { _AccountLVM = value; OnPropertyChanged("AccountLVM"); } } internal AccountListViewModel GetAccounts() { _AccountLVM = new AccountListViewModel(); _AccountLVM.AccountList.Clear(); foreach (Account i in context.Accounts.Where(x=> x.ApplicationId == this.Id)) { AccountViewModel account = new AccountViewModel(i); account.Application = this; _AccountLVM.AccountList.Add(account); } return _AccountLVM; } } 

AccountListViewModel

 class AccountListViewModel : ViewModelBase { myEntities context = new myEntities(); private static AccountListViewModel instance = null; private ObservableCollection<AccountViewModel> _accountList = null; public ObservableCollection<AccountViewModel> AccountList { get { if (_accountList != null) return _accountList; else return GetAccounts(); } set { _accountList = value; OnPropertyChanged("AccountList"); } } private AccountViewModel selectedAccount = null; public AccountViewModel SelectedAccount { get { return selectedAccount; } set { selectedAccount = value; OnPropertyChanged("SelectedAccount"); } } public AccountListViewModel() { this._accountList = GetAccounts(); } internal ObservableCollection<AccountViewModel> GetAccounts() { if (_accountList == null) _accountList = new ObservableCollection<AccountViewModel>(); _accountList.Clear(); foreach (Account item in context.Accounts) { AccountViewModel a = new AccountViewModel(item); _accountList.Add(a); } return _accountList; } public static AccountListViewModel Instance() { if (instance == null) instance = new AccountListViewModel(); return instance; } } 

AccountViewModel. I simply exclude all initialization logic in the viewmodel for simplicity.

 class AccountViewModel : ViewModelBase { private myEntites context = new myEntities(); private AccountViewModel originalValue; public AccountViewModel() { } public AccountViewModel(Account acc) { //Assign property values. this.originalValue = (AccountViewModel)this.MemberwiseClone(); } public AccountListViewModel Container { get { return AccountListViewModel.Instance(); } } public ApplicationViewModel Application { get; set; } } 

Edit1:
When I bind data to view SelectedAccount details with a text box, it does not display text.
1. Ability to bind data to ApplicationListViewModel for Combobox.
2. Successfully Bind to view AccountList based on SelectedApplication
3. Failed to bind to the selected account in AccountListViewModel.

I think that in the next line it does not show any information about the selected account. I checked all the data binding syntaxes. In properties, I can view the corresponding DataContext and bind it to the properties. But the text is not displayed. When I select each individual record in the DataGrid, I can debug the call and select the object, but somehow this object does not appear in the text box at the very end.

 DataContext="{Binding SelectedApplication.AccountLVM.SelectedAccount}" 

Edit2:
Based on the suggestion in the comment below, I tried snoop and was able to see the header text box row highlighted in red. I am trying to change the binding property of Path and datacontext, but still not working. When I tried to click "Delve Binding Expression", it gave me an unhandled exception. I do not know what this means if it came from Snoop.

Edit3:
I took screenshots of the DataContext property for the StackPanel for the Account Information section and the text property of the text field.

enter image description here

Decision:
Based on the suggestions below, I made the following changes to my decision and made it simpler. I made it unnecessarily complicated.
1. AccountsViewModel
2. AccountViewModel
3. ApplicationViewModel

Now I created properties as SelectedApplication , SelectedAccount with just one AccountsViewModel . The entire complex DataContext syntax has been removed, and now there is only one DataContext on the xaml page.

Simplified code.

 class AccountsViewModel: ViewModelBase { myEntities context = new myEntities(); private ObservableCollection<ApplicationViewModel> _ApplicationList = null; public ObservableCollection<ApplicationViewModel> ApplicationList { get { if (_ApplicationList == null) { GetApplications(); } return _ApplicationList; } set { _ApplicationList = value; OnPropertyChanged("ApplicationList"); } } internal ObservableCollection<ApplicationViewModel> GetApplications() { if (_ApplicationList == null) _ApplicationList = new ObservableCollection<ApplicationViewModel>(); else _ApplicationList.Clear(); foreach (Application item in context.Applications) { ApplicationViewModel a = new ApplicationViewModel(item); _ApplicationList.Add(a); } return _ApplicationList; } //Selected Application Property private ApplicationViewModel selectedApplication = null; public ApplicationViewModel SelectedApplication { get { return selectedApplication; } set { selectedApplication = value; this.GetAccounts(); OnPropertyChanged("SelectedApplication"); } } private ObservableCollection<AccountViewModel> _accountList = null; public ObservableCollection<AccountViewModel> AccountList { get { if (_accountList == null) GetAccounts(); return _accountList; } set { _accountList = value; OnPropertyChanged("AccountList"); } } //public ObservableCollection<AccountViewModel> Cu private AccountViewModel selectedAccount = null; public AccountViewModel SelectedAccount { get { return selectedAccount; } set { selectedAccount = value; OnPropertyChanged("SelectedAccount"); } } internal ObservableCollection<AccountViewModel> GetAccounts() { if (_accountList == null) _accountList = new ObservableCollection<AccountViewModel>(); else _accountList.Clear(); foreach (Account item in context.Accounts.Where(x => x.ApplicationId == this.SelectedApplication.Id)) { AccountViewModel a = new AccountViewModel(item); _accountList.Add(a); } return _accountList; } } 

XAML side

 <UserControl.Resources> <vm:AccountsViewModel x:Key="ALVModel" /> </UserControl.Resources> <StackPanel DataContext="{Binding Source={StaticResource ALVModel}}" Margin="0,0,-390,-29"> <StackPanel> <ComboBox x:Name="cbxApplicationList" ItemsSource="{Binding Path=ApplicationList}" DisplayMemberPath="Title" SelectedValuePath="Id" SelectedItem="{Binding Path=SelectedApplication, Mode=TwoWay}" IsSynchronizedWithCurrentItem="True"></ComboBox> <DataGrid x:Name="dtgAccounts" Height="Auto" Width="auto" AutoGenerateColumns="False" ItemsSource="{Binding Path=AccountList}" SelectedItem="{Binding SelectedAccount, Mode=TwoWay}" IsSynchronizedWithCurrentItem="True" > <DataGrid.Columns> <DataGridTextColumn Header="Title" Binding="{Binding Path=Title}"></DataGridTextColumn> <DataGridTextColumn Header="CreatedDate" Binding="{Binding Path=CreatedDate}"></DataGridTextColumn> <DataGridTextColumn Header="LastModified" Binding="{Binding Path=LastModifiedDate}"></DataGridTextColumn> </DataGrid.Columns> </DataGrid> </StackPanel> <StackPanel Height="Auto" Width="300" HorizontalAlignment="Left" DataContext="{Binding Path=SelectedAccount}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="30"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="100"></ColumnDefinition> <ColumnDefinition Width="200"></ColumnDefinition> </Grid.ColumnDefinitions> <TextBlock x:Name="lblTitle" Grid.Row="0" Grid.Column="0" >Title</TextBlock> <TextBox x:Name="txtTitle" Grid.Row="0" Grid.Column="1" Height="30" Width="200" Text="{Binding Title}"></TextBox> </Grid> </StackPanel> </StackPanel> 

I did not understand the concept of MVVM properly. I tried to build everything modular, and in the end I messed it up.

+4
source share
2 answers

I suspect your problem is that you are returning a new ObservableCollection every time you call setter for AccountLVM and you do not raise a PropertyChange notification, so any existing bindings are not updated

 public AccountListViewModel AccountLVM { get { return GetAccounts(); } set { _AccountLVM = value; OnPropertyChanged("AccountLVM"); } } internal AccountListViewModel GetAccounts() { _AccountLVM = new AccountListViewModel(); _AccountLVM.AccountList.Clear(); foreach (Account i in context.Accounts.Where(x=> x.ApplicationId == this.Id)) { AccountViewModel account = new AccountViewModel(i); account.Application = this; _AccountLVM.AccountList.Add(account); } return _AccountLVM; } 

I think your bindings are very confusing and difficult to follow, however I think that whenever this is evaluated

 DataContext="{Binding SelectedApplication.AccountLVM.SelectedAccount}" 

it creates a new AccountLVM that does not have the property of the SelectedAccount property.

You do not see the existing DataGrid.SelectedItem change at all, because it is still bound to the old AccountLVM , because the PropertyChange notification was raised when _accountLVM changed, so the binding is not aware of the update.

But some other different ones related to your code:

  • Do not change the personal version of the property unless you raise the PropertyChange notification for the public version of the property. This applies to both your constructors and your GetXxxxx() methods, such as GetAccounts() .

  • Do not return a method call from your recipient. Instead, set the value by calling the method if it is null, and then return the private property.

     public AccountListViewModel AccountLVM { get { if (_accountLVM == null) GetAccounts(); // or _accountLVM = GetAccountLVM(); return _accountLVM; } set { ... } } 
  • This is really confusing that the DataContext set in a variety of controls. DataContext is the data layer behind your user interface, and this is easiest if your user interface simply reflects the data level, and you need to navigate everywhere to get your data, which makes tracking data levels very difficult.

    / li>
  • If you need to bind to something other than the current data context, try using other binding properties to specify a different Source binding before changing the DataContext immediately. Here is an example of using the ElementName property to set the binding source:

     <TextBox x:Name="txtTitle" ... Text="{Binding ElementName=dtgAccounts, Path=SelectedItem.Title}" /> 
  • DataContext in inherited, so you do not need to write DataContext="{Binding }"

  • You might want to consider re-writing the parent ViewModel so that you can configure XAML like this without additional DataContext bindings or nested three-part properties.

     <ComboBox ItemsSource="{Binding ApplicationList}" SelectedItem="{Binding SelectedApplication}" /> <DataGrid ItemsSource="{Binding SelectedApplication.Accounts}" SelectedItem="{Binding SelectedAccount}" /> <StackPanel DataContext="{Binding SelectedAccount}"> ... </StackPanel> 

If you are new to DataContext or trying to figure it out, I would recommend reading this article on my blog to better understand what it is and how it works.

+3
source

Well, one of the main problems with this Binding method is that the value is updated only when the last property value in your SelectedAccount case changes. The remaining levels are not tracked by BindingExpression , so if, for example, SelectedApplication.AccountLVM changed, the DataContext does not notice the difference in SelectedAccount , because the binding still "looks" at the old link and you change the other link in your virtual machine.

Therefore, I think that at the beginning of the application, SelectedApplication is null, and the Binding ComboBox does not notice that it is changing. Hmm, I thought of another binding solution, but I could not find it. Therefore, I suggest creating an additional property to display the SelectedAccount in your ApplicationListViewModel class.

+2
source

All Articles