rotormonkey
rotormonkey

Reputation: 47

WPF unexpected ICollectionView behavior

I'm new to WPF, and just learning how things work. My endgoal is to have a datagrid for each heading which displays totals from a collection of items associated with each category.

But I'm seeing some unexpected behavior. I expect to see this output:

Heading1
Category1
Category2

Heading2
Category3

What I actually see is this:

Heading1
Category3

Heading2
Category3

I'm stumped as to why. Hoping somebody could shed a little light. Thanks for any help!

Here's my code:

MainWindow.xaml

<Window x:Class="TestApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <ItemsControl ItemsSource="{Binding Headings}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding Description}" />
                        <DataGrid ItemsSource="{Binding Categories}" AutoGenerateColumns="False">
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="" Binding="{Binding Description}" Width="*" />
                            </DataGrid.Columns>
                        </DataGrid>
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</Window>

MainWindow.xaml.cs

    public partial class MainWindow : Window
    {
        public ObservableCollection<HeadingViewModel> Headings { get; }

        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;

            var category1 = new CategoryViewModel()
            {
                Id = 1,
                Description = "Category 1",
                HeadingId = 1,
            };

            var category2 = new CategoryViewModel()
            {
                Id = 2,
                Description = "Category 2",
                HeadingId = 1,
            };

            var category3 = new CategoryViewModel()
            {
                Id = 3,
                Description = "Category 3",
                HeadingId = 2,
            };

            var categories = new ObservableCollection<CategoryViewModel>();
            categories.Add(category1);
            categories.Add(category2);
            categories.Add(category3);

            var heading1 = new HeadingViewModel(categories)
            {
                Id = 1,
                Description = "Heading 1",
            };

            var heading2 = new HeadingViewModel(categories)
            {
                Id = 2,
                Description = "Heading 2",
            };

            this.Headings = new ObservableCollection<HeadingViewModel>();
            this.Headings.Add(heading1);
            this.Headings.Add(heading2);
        }

HeadingViewModel.cs

public class HeadingViewModel : INotifyPropertyChanged
    {
        public ICollectionView Categories { get; }

        public HeadingViewModel(ObservableCollection<CategoryViewModel> categories)
        {
            this.Categories = CollectionViewSource.GetDefaultView(categories);
            this.Categories.Filter = CategoriesFilter;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private bool CategoriesFilter(object item)
        {
            var category = item as CategoryViewModel;
            return category.HeadingId == this.Id;
        }

        private int id;
        public int Id
        {
            get
            {
                return id;
            }
            set
            {
                this.id = value;
                OnPropertyChanged();
                Categories.Refresh();
            }
        }

        public string Description { get; set; }

        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }

CategoryViewModel.cs

public class CategoryViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public CategoryViewModel() {}

        public int Id { get; set; }

        public string Description { get; set; }

        private int? headingId;
        public int? HeadingId
        {
            get
            {
                return headingId;
            }
            set
            {
                this.headingId = value;
                OnPropertyChanged();
            }
        }

        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }

Upvotes: 1

Views: 71

Answers (2)

rotormonkey
rotormonkey

Reputation: 47

grek40 gets the gold star for telling me what the problem was - but here's the solution for anyone else that stumbles across this:

In HeadingViewModel.cs change this line:

this.Categories = CollectionViewSource.GetDefaultView(categories);

to this:

this.Categories = new CollectionViewSource { Source = categories }.View;

That returns a new instance of the viewsource for each instance of Heading which solves the issue.

Upvotes: 1

grek40
grek40

Reputation: 13448

The problem is, CollectionViewSource.GetDefaultView(categories) will return the same ICollectionView instance for the same input, no matter how often you call it.

Since you pass the same categories to both HeadingViewModel constructors, the second constructor this.Categories.Filter = CategoriesFilter will override the filter for both headings.

Upvotes: 1

Related Questions