在 WPF 中使用 MVVM 的嵌套数据绑定不起作用

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15487709/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 08:08:55  来源:igfitidea点击:

Nested Data Binding using MVVM in WPF not working

c#wpfentity-frameworkmvvm

提问by Mitul

I am not able to figure out why my third Nested DataBinding in WPF is not working. I am using Entity Framework and Sql Server 2012 and following are my entities. An Application can have more than one accounts. There is an Accounts Table and an Applications Table.

我无法弄清楚为什么我在 WPF 中的第三个嵌套数据绑定不起作用。我正在使用实体框架和 Sql Server 2012,以下是我的实体。一个应用程序可以有多个帐户。有一个帐户表和一个应用程序表。

ENTITIES
1. Applications
2. Accounts

实体
1. 申请
2. 账户

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

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

In my usercontrol I am trying to do following:
1. Use combobox to select an application using ApplicationListViewModel (Working)
2. Upon selected application display all accounts in datagrid (Working)
3. Upon selected account display details information about a particular account.(Does not show details of the selected account)

在我的用户控件中,我尝试执行以下操作:
1. 使用组合框选择使用 ApplicationListViewModel ( Working)的应用程序
2. 在选定的应用程序中显示数据网格中的所有帐户 ( Working)
3. 在选定的帐户上显示有关特定帐户的详细信息。(不显示所选帐户的详细信息

<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 am eliminating all other initialization logic aside in 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 data bind to view the details of the SelectedAccount with textbox it doesn't show any text.
1. Able to databind to ApplicationListViewModel to Combobox.
2. Successfully Bind to view AccountList based upon SelectedApplication
3. Unable to Bind to SelectedAcount in the AccountListViewModel.

Edit1:
当我数据绑定以使用文本框查看 SelectedAccount 的详细信息时,它不显示任何文本。
1. 能够将数据绑定到 ApplicationListViewModel 到 Combobox。
2. 成功绑定以查看基于 SelectedApplication 的 AccountList
3. 无法绑定到 AccountListViewModel 中的 SelectedAcount。

I think in the following line it doesn't show any details about the selected account. I have checked all databinding syntax. In the properties I am able to view appropriate DataContext and bind to the properties. But it doesn't show any text. When I select each individual record in the DataGrid I am able to debug the call and select the object but somehow that object is not being shown in the textbox at the very end.

我认为在下一行中它没有显示有关所选帐户的任何详细信息。我已经检查了所有数据绑定语法。在属性中,我能够查看适当的 DataContext 并绑定到属性。但它不显示任何文本。当我在 DataGrid 中选择每个单独的记录时,我能够调试调用并选择对象,但不知何故,该对象最终没有显示在文本框中。

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

Edit2:
Based upon the suggestion in the comment below I tried snoop and was able to see the title textbox row highlighted in red color. I am trying to change the binding Path property and datacontext but still not working. When I tried to click on the "Delve Binding Expression" it gave me unhandled exception. I don't know what that means if as it came from Snoop.

Edit2:
根据下面评论中的建议,我尝试了 snoop,并且能够看到以红色突出显示的标题文本框行。我正在尝试更改绑定路径属性和数据上下文,但仍然无法正常工作。当我尝试单击“Delve Binding Expression”时,它给了我未处理的异常。如果它来自 Snoop,我不知道这意味着什么。

Edit3:
I have taken screenshots of DataContext Property for the StackPanel for the Account Details section and the text property of the textbox.

Edit3:
我为帐户详细信息部分的 StackPanel 和文本框的文本属性截取了 DataContext 属性的屏幕截图。

enter image description here

在此处输入图片说明

Solution:
Based upon suggestions below I have made following changes to my solution and made it way more simple. I made it unnecessarily complex.
1. AccountsViewModel
2. AccountViewModel
3. ApplicationViewModel

解决方案:
根据以下建议,我对我的解决方案进行了以下更改,使其变得更加简单。我让它变得不必要地复杂。
1. AccountsViewModel
2. AccountViewModel
3. ApplicationViewModel

Now I have created properties as SelectedApplication, SelectedAccountall in just one AccountsViewModel. Removed all complex DataContext syntax and now there is just one DataContext in the xaml page.

现在我已经将属性创建为SelectedApplicationSelectedAccount全部都在一个中 AccountsViewModel。删除了所有复杂的 DataContext 语法,现在 xaml 页面中只有一个 DataContext。

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

XAML 端

<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 didn't understood MVVM concept properly. I tried to build everything modular and in the end I screwed it up.

我没有正确理解 MVVM 的概念。我试图构建所有模块化的东西,最后我把它搞砸了。

采纳答案by Rachel

I suspect your problem is related to the fact you are returning a newObservableCollectionevery time you call the setter for AccountLVM, and you are not raising your PropertyChangenotification, so any existing bindings do not get updated

我怀疑您的问题与每次调用 setter for 时都返回一个的事实ObservableCollection有关AccountLVM,并且您没有PropertyChange发出通知,因此任何现有绑定都不会更新

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 find your bindings very confusing and hard to follow, however I think whenever this gets evaluated

我发现您的绑定非常混乱且难以理解,但是我认为每当对此进行评估时

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

it is creating a newAccountLVM, which does not have the SelectedAccountproperty set.

它正在创建一个没有设置属性的newAccountLVMSelectedAccount

You don't see the existing DataGrid.SelectedItemchange at all because it's still bound to the oldAccountLVMas no PropertyChangenotification got raised when _accountLVMchanged, so the binding doesn't know to update.

您根本看不到现有的DataGrid.SelectedItem更改,因为它仍然绑定到旧的AccountLVM,因为更改时没有PropertyChange引发通知_accountLVM,因此绑定不知道更新。

But some other miscellaneous related to your code:

但是与您的代码相关的其他一些杂项:

  • Don't change the private version of the property unless you also raise the PropertyChange notification for the public version of the property. This applies to both your constructors and your GetXxxxx()methods like GetAccounts().

  • Don't return a method call from your getter. Instead set the value using your method call if it's null, and return the private property afterwards.

    public AccountListViewModel AccountLVM
    {
        get
        {
            if (_accountLVM == null)
                GetAccounts(); // or _accountLVM = GetAccountLVM();
    
            return _accountLVM;
        }
        set { ... }
    }
    
  • It's really confusing to have the DataContextset in so many controls. The DataContextis the data layer behind your UI, and it's easiest if your UI simply reflects the data layer, and having to go all over the place to get your data makes the data layer really hard to follow.

  • If you must make a binding to something other than the current data context, try to use other binding properties to specify a different binding Sourcebefore immediately going to change the DataContext. Here's an example using the ElementNameproperty to set the binding source:

    <TextBox x:Name="txtTitle" ...
             Text="{Binding ElementName=dtgAccounts, Path=SelectedItem.Title}" />
    
  • The DataContextin inherited, so you don't ever need to write DataContext="{Binding }"

  • You may want to consider re-writing your parent ViewModel so you can setup XAML like this, without all the extra DataContextbindings or 3-part nested properties.

    <ComboBox ItemsSource="{Binding ApplicationList}"
              SelectedItem="{Binding SelectedApplication}" />
    
    <DataGrid ItemsSource="{Binding SelectedApplication.Accounts}"
              SelectedItem="{Binding SelectedAccount}" />
    
    <StackPanel DataContext="{Binding SelectedAccount}">
       ...
    </StackPanel>
    
  • 不要更改该属性的私有版本,除非您还为该属性的公共版本引发 PropertyChange 通知。这适用于您的构造函数和您的GetXxxxx()方法,例如GetAccounts().

  • 不要从你的 getter 返回方法调用。如果它为空,则使用您的方法调用设置该值,然后返回私有属性。

    public AccountListViewModel AccountLVM
    {
        get
        {
            if (_accountLVM == null)
                GetAccounts(); // or _accountLVM = GetAccountLVM();
    
            return _accountLVM;
        }
        set { ... }
    }
    
  • DataContext在这么多控件中设置设置真的很令人困惑。这DataContext是 UI 背后的数据层,如果您的 UI 只是反映数据层,那么最简单,并且不得不到处获取数据使得数据层很难跟踪。

  • 如果必须绑定到当前数据上下文以外的内容,请尝试使用其他绑定属性指定不同的绑定,Source然后立即更改DataContext. 以下是使用该ElementName属性设置绑定源的示例:

    <TextBox x:Name="txtTitle" ...
             Text="{Binding ElementName=dtgAccounts, Path=SelectedItem.Title}" />
    
  • DataContext在继承的,这样你就不会永远需要写DataContext="{Binding }"

  • 您可能需要考虑重写您的父 ViewModel,以便您可以像这样设置 XAML,而无需所有额外的DataContext绑定或 3 部分嵌套属性。

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

If you're new to the DataContextor struggling to understand it, I'd recommend reading this articleon my blog to get a better understanding of what it is and how it works.

如果您不DataContext熟悉它或难以理解它,我建议您阅读我博客上的这篇文章,以更好地了解它是什么以及它是如何工作的。

回答by DHN

Well one major problem with this Bindingmethod is, that the value is only updated, when the last property value, in your case SelectedAccount, is changed. The other levels are not watched by the BindingExpression, so if e.g. SelectedApplication.AccountLVMis changed the DataContextwill not notice a difference in SelectedAccountbecause the binding is still 'watching' on the old reference and you're modifying another reference in your VM.

这种Binding方法的一个主要问题是,仅当最后一个属性值(在您的情况下SelectedAccount)发生更改时才会更新该值。其他级别不会被 监视BindingExpression,因此如果SelectedApplication.AccountLVM更改了eg ,DataContext则不会注意到其中的差异,SelectedAccount因为绑定仍在“监视”旧引用,并且您正在修改 VM 中的另一个引用。

So I think at the start of the application SelectedApplicationis null and the Bindingof the ComboBoxdoesn't notice that it changes. Hmm, I thought about another binding solution, but I couldn't found one. So I suggest, that you create an additional property for reflecting SelectedAccountin your ApplicationListViewModelclass.

所以我认为在应用程序的开始SelectedApplication是空和BindingComboBox,它改变不另行通知。嗯,我想到了另一种绑定解决方案,但我找不到。所以我建议你创建一个额外的属性来反映SelectedAccount你的ApplicationListViewModel类。