2016-11-18 9 views
0

Мой коллега и я отчаянно пытались понять, почему мы не можем получить коллекцию ViewModels для рендеринга, как ожидалось. Мы создали очень простой пример, демонстрирующий проблему.WPF привязка к коллекции ViewModels не отображается, как ожидалось

В принципе, у нас есть класс StupidPerson, который имеет имя и список друзей (также StupidPerson's). В MainViewModel мы создаем корень StupidPerson и добавляем к своим друзьям четыре других StupidPerson's. MainWindow просто отображает источник StupidPerson с помощью StupidPersonViewModel.

У StupidPersonViewModel есть все колокола и свистки, а код за StupidPersonView даже реализует DependencyProperty. StupidPersonView связывает ItemsSource элемента Items с свойством StupidFriends StupidPersonViewModel.

У нас есть, конечно, сложные вещи, чтобы попробовать всевозможные возможности. То, что я ожидаю увидеть от XAML ниже, - «Name: Fred», а затем «Friends:», а затем четыре «Name: XXXX» и пустые списки «Друзья:». Однако я получаю 4 пустых StupidPerson's.

Что происходит, так это то, что вместо использования StupidPersonViewModel, созданного в MainViewModel, привязанного к ItemsSource, магия XAML обновляет четыре пустых StupidPersonViewModel и использует их для рендеринга элементов. Очевидно, он связан с созданным мной списком, потому что он отображает только 4 пустых ViewModels.

Полностью сбит с толку.

<UserControl x:Class="StupidXaml.StupidPersonView" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
     xmlns:local="clr-namespace:StupidXaml" 
     mc:Ignorable="d" 
     d:DesignHeight="300" 
     Background="White" Width="509.016"> 
<UserControl.DataContext> 
    <local:StupidPersonViewModel /> 
</UserControl.DataContext> 
<StackPanel> 
    <Label Content="{Binding Name}" /> 

    <Label Content="Friends:" /> 
    <ItemsControl Margin="10,0,0,0" ItemsSource="{Binding StupidFriends}"> 
     <ItemsControl.ItemTemplate> 
      <DataTemplate> 
       <local:StupidPersonView /> 
      </DataTemplate> 
      <!--<DataTemplate DataType="local:StupidPersonViewModel"> 
       <StackPanel Orientation="Horizontal"> 
        --><!-- Proves that binding is a StupidPersonViewModel --><!-- 
        <Label Content="{Binding}"></Label> 
        --><!-- Both of these work! --><!-- 
        <Label Content="{Binding Name}"></Label> 
        <Label Content="{Binding Person.Name}"></Label> 

        --><!-- But none of these work. How is this possible!? --> 
        <!-- DataContext binding --> 
        <!--<local:StupidPersonView DataContext="{Binding}" /> 
        <local:StupidPersonView DataContext="{Binding DataContext, ElementName=item}" />--> 
        <!-- Dependency Property binding --> 
        <!--<local:StupidPersonView Person="{Binding Person}" /> 
        <local:StupidPersonView Person="{Binding DataContext.Person, ElementName=item}" /> 
        <local:StupidPersonView Person="{Binding DataContext.Person, ElementName=item, BindsDirectlyToSource=True}" x:Name="self" />--><!-- 
       </StackPanel> 
      </DataTemplate>--> 
     </ItemsControl.ItemTemplate> 
    </ItemsControl> 
</StackPanel> 

Показывает это: simplest attempt

и это XAML

<UserControl x:Class="StupidXaml.StupidPersonView" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
     xmlns:local="clr-namespace:StupidXaml" 
     mc:Ignorable="d" 
     d:DesignHeight="300" 
     Background="White" Width="509.016"> 
<UserControl.DataContext> 
    <local:StupidPersonViewModel /> 
</UserControl.DataContext> 
<StackPanel> 
    <Label Content="{Binding Name}" /> 

    <Label Content="Friends:" /> 
    <ItemsControl Margin="10,0,0,0" ItemsSource="{Binding StupidFriends}"> 
     <ItemsControl.ItemTemplate> 
      <!--<DataTemplate> 
       <local:StupidPersonView /> 
      </DataTemplate>--> 
      <DataTemplate DataType="local:StupidPersonViewModel"> 
       <StackPanel Orientation="Horizontal"> 
        <!--Proves that binding is a StupidPersonViewModel--> 
        <Label Content="{Binding}"></Label> 
        <!--Both of these work!--> 
        <Label Content="{Binding Name}"></Label> 
        <Label Content="{Binding Person.Name}"></Label> 

        <!--But none of these work. How is this possible!?--> 
        <!--DataContext binding--> 
        <local:StupidPersonView DataContext="{Binding}" /> 
        <local:StupidPersonView DataContext="{Binding DataContext, ElementName=item}" /> 
        <!--Dependency Property binding--> 
        <local:StupidPersonView Person="{Binding Person}" /> 
        <local:StupidPersonView Person="{Binding DataContext.Person, ElementName=item}" /> 
        <local:StupidPersonView Person="{Binding DataContext.Person, ElementName=item, BindsDirectlyToSource=True}" x:Name="self" /> 
       </StackPanel> 
      </DataTemplate> 
     </ItemsControl.ItemTemplate> 
    </ItemsControl> 
</StackPanel> 

отображает это: all other attempts

public class MainViewModel 
{ 
    public StupidPersonViewModel Source { get; set; } 

    public MainViewModel() 
    { 
     Source = new StupidPersonViewModel { Person = new StupidPerson { Name = "Fred" } }; 

     Source.Person.StupidFriends.Add(new StupidPerson { Name = "Bob" }); 
     Source.Person.StupidFriends.Add(new StupidPerson { Name = "Greg" }); 
     Source.Person.StupidFriends.Add(new StupidPerson { Name = "Frank" }); 
     Source.Person.StupidFriends.Add(new StupidPerson { Name = "Tommy" }); 
    } 
} 

public class StupidPersonViewModel : INotifyPropertyChanged 
{ 
    [CanBeNull] 
    public string Name => $"Name: {this.Person?.Name}"; 

    private StupidPerson person; 

    [CanBeNull] 
    public StupidPerson Person 
    { 
     get { return this.person; } 
     set 
     { 
      this.person = value; 

      this.RaisePropertyChanged(nameof(this.Person)); 

      this.StupidFriends = new ObservableCollection<StupidPersonViewModel>(); 
      foreach (var friend in value.StupidFriends) 
      { 
       this.StupidFriends.Add(new StupidPersonViewModel { Person = friend }); 
      } 


      this.RaisePropertyChanged(nameof(this.Name)); 
      this.RaisePropertyChanged(nameof(this.StupidFriends)); 
     } 
    } 

    private void RaisePropertyChanged(string property) 
    { 
     this.OnPropertyChanged(property); 
    } 

    private ObservableCollection<StupidPersonViewModel> stupidFriends; 

    public ObservableCollection<StupidPersonViewModel> StupidFriends 
    { 
     get { return this.stupidFriends; } 
     set 
     { 
      this.stupidFriends = value; 

      this.RaisePropertyChanged(nameof(this.StupidFriends)); 
     } 
    } 


    //public StupidPersonViewModel() 
    //{ 
    //} 

    //public StupidPersonViewModel(StupidPerson person) 
    //{ 
    // this.Person = person; 
    //} 

    public event PropertyChangedEventHandler PropertyChanged; 

    [NotifyPropertyChangedInvocator] 
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 
    { 
     this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 
    } 
} 
+0

Следует ли сначала создать StupidPerson Fred и друзей Fred, а затем создать модель представления и назначить свойство Person, поскольку у вас есть логика в установщике Person в StupidPersonViewModel для создания ViewModels из списка StupidFriends? –

ответ

2

Широко сделал ошибку в реализации UserControl является явно установить его свойство DataContext к экземпляру ожидаемой модели представления, как вы делаете на

<UserControl.DataContext> 
    <local:StupidPersonViewModel /> 
</UserControl.DataContext> 

Это эффективно предотвращает, что UserControl наследует DataContext от родительского контроля, как ожидается, в

<ItemsControl.ItemTemplate> 
    <DataTemplate> 
     <local:StupidPersonView /> 
    </DataTemplate> 
</ItemsControl.ItemTemplate> 

, где наследуемое DataContext является элементом в ItemsSource коллекции.

Наследование здесь означает Dependency Property Value Inheritance.


Так что просто не указывайте явным образом DataContext UserControl. Никогда. Любые блоги или онлайн-уроки, рассказывающие вам об этом, являются неправильными.

+1

Кроме того, вы должны использовать TreeView для отображения иерархически структурированных данных. – Clemens

+0

Спасибо. Мы действительно тестировали это (то есть нет явного UserControl DataContext), но я думаю, что среди наших упражнений по вытягиванию волос так и не вернулись. Совершенно невероятно, сколько мест в Интернете вы обнаружите, что DataContext явно не рекомендуется. Я бы рискнул сказать, что это ** большинство ** из них! В дополнение к тому, что на самом деле все ** работает **, конструктор VS ** намного счастливее! Кроме того, это было что-то другое, кроме теста Stupid, мы определенно рассмотрим использование TreeView. –

+0

Я хотел бы повторить, что нужно установить DataContext хотя бы раз, разумеется. В нашем реальном приложении у нас есть элемент управления вкладками с несколькими вкладками. Каждая вкладка содержит диспетчер компоновки, который, в свою очередь, содержит UserControl. Мы ** должны ** установить DataContext явно для каждой вкладки в ViewModel, который воздействует на каждый UserControl. В противном случае нам нужно было бы поместить весь ViewModel в MainWindowViewModel. –