-->

WPF binding to a collection of ViewModels fails to

2019-03-04 17:21发布

问题:

My colleague and I have been desperately trying to understand why we can't get a collection of ViewModels to render as expected. We have created a very simple example that demonstrates the issue.

Basically, we have a StupidPerson class that has a Name and a List of friends (also StupidPerson's). In the MainViewModel we create the root StupidPerson and add to his friends four other StupidPerson's. The MainWindow simply displays the source StupidPerson using the StupidPersonViewModel.

The StupidPersonViewModel has all the bells and whistles and the code behind the StupidPersonView even implements a DependencyProperty. The StupidPersonView binds the ItemsSource of an ItemsControl to the StupidFriends property of the StupidPersonViewModel.

We have certainly overcomplicated things in order to try all the different possibilities. What I would expect to see from the XAML below is "Name: Fred" followed by "Friends:" followed by four "Name: XXXX" and empty "Friends:" lists. However, what I get is 4 empty StupidPerson's.

What is happening is that instead of using the StupidPersonViewModel's that I created in MainViewModel which are bound to ItemsSource, XAML magic is newing up four empty StupidPersonViewModel's and using them for the items to render. It obviously is bound to the list I created because it only renders 4 empty ViewModels.

Totally baffled.

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

Displays this: simplest attempt

and this 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>

displays this: 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));
    }
}

回答1:

A commonly made mistake in the implementation of a UserControl is to explicitly set its DataContext property to an instance of the expected view model, as you do by

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

Doing so effectively prevents that the UserControl inherits a DataContext from its parent control, as is expected in

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

where the inherited DataContext is an element of the ItemsSource collection.

Inheritance here means Dependency Property Value Inheritance.


So just don't explicitly set a UserControl's DataContext. Never. Any blogs or online tutorials telling you so are plain wrong.