ObservableCollection Vs List In C#

Introduction

Today we will learn the subtle difference between ObservableCollection and List. In nutshell both List and ObservableCollection inherits ICollection, IList interfaces. both are a collection of objects.

So when we already have a List why do we need ObservableCollection?

If you look at the implementation of ObservableCollection, You would see that it inherits INotifyCollectionChanged, INotifyPropertyChanged as shown in figure 1. 

Figure 1: Class ObservableCollection

INotifyCollectionChanged

In figure 1 you can see there is NotifyCollectionChangedEventHandler, this is CollectionChanged event which notifies the listener whenever an item has been added or removed from the list.

INotifyPropertyChanged

In the above figure, there is event PropertyChangedEventHandler, which is PropertyChanged, this notifies the listener when value of the property has been changed.

Well this is what List is missing, which makes ObservableCollection so special.

This is good in theory, but we need to understand it practically. Let's create a small WPF application.

The APP

This is a WPF application, following the MVVM pattern. The object we are going to work with is Actor. Our screen which is MainWindow will have a ListBox to show the details of actors. A small form to create a new actor object with 2 buttons, one to create and another to alter the actor's details. Alright, having said that go ahead and create a WPF application. And inside a MainWindow adds the following code. This listing contains everything that is mentioned above.

<Window x:Class="ObservableCollectionVsList.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:ObservableCollectionVsList" 
        mc:Ignorable="d"
        Title="MainWindow" Height="320" Width="700">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBlock FontSize="15"
                   Foreground="DarkBlue"
                   HorizontalAlignment="Center"
                   Margin="5"
                   Text="Actors details:"/>
        
        <ListView x:Name="ListViewAllActors"
                  Background="#EEEEEE"
                  HorizontalAlignment="Center"
                  ItemsSource="{Binding ListOfActors}"
                  Grid.Row="1">
            <ListView.ItemTemplate>
                <DataTemplate>
                <StackPanel Orientation="Horizontal" 
                            Width="520">
                    <TextBlock x:Name="TextBlockActorDetail"
                           FontWeight="Bold" > 
                         <Run Text=" | "/>
                         <Run Text="Name: "
                              Foreground="#344CB7"/>								 								
                         <Run Text="{Binding Name}"
                              Foreground="#CD1818"/>	
                         <Run Text=" | "/>
                         <Run Text="Age: "
                              Foreground="#344CB7"/>								 								
                         <Run Text="{Binding Age}"
                              Foreground="#CD1818"/>
                         <Run Text=" | "/>	
                         <Run Text="DOB: "
                              Foreground="#344CB7"/>								 								
                         <Run Text="{Binding DOB, StringFormat=D}" 
                              Foreground="#CD1818"/>
                         <Run Text=" | Oscar: "
                              Foreground="#344CB7"/>
                        </TextBlock>
                    <CheckBox IsChecked="{Binding WonOscar}"/>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        
        <Border Background="#11324D" 
                BorderBrush="White" 
                BorderThickness="1,1,1,1" 
                CornerRadius="30,30,30,30"
                HorizontalAlignment="Center"
                Margin="0 5 0 0"
                Grid.Row="3">
            <Grid x:Name="GridPost"
                  Margin="10 10 10 0"
                  Width="260">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="5*"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>

                <Label x:Name="LabelActorName"        
                      Content="Actor Name:"  
                      Foreground="White"/>

                <Label x:Name="LabelDOB"         
                       Content="DOB:"  
                       Foreground="White"
                       Grid.Row="1"/>

                <Label x:Name="LabelAge"         
                       Content="Age:"  
                       Foreground="White"
                       Grid.Row="2"/>
                
                <Label x:Name="LabelOscar"         
                       Content="Oscar:"  
                       Foreground="White"
                       Grid.Row="3"/>

                <TextBox  x:Name="TextBoxActorName"
                         Text="{Binding ActorName}"
                         Height="20"        
                         Width="150"        
                         Grid.Column="1"/>
                
                <TextBox x:Name="TextBoxDOB"  
                         Text="{Binding ActorDOB, StringFormat=d}"
                         Height="20"        
                         Width="150"        
                         Grid.Column="1"        
                         Grid.Row="1"/>
                
                <TextBox  x:Name="TextBoxActorAge"   
                         Text="{Binding ActorAge}"
                         Height="20"        
                         Width="150"        
                         Grid.Column="1"        
                         Grid.Row="2"/>
                
                <CheckBox x:Name="CheckBoxOscar"
                          IsChecked="{Binding ActorWonOscar}"
                          Margin="0 4 0 0"
                          Grid.Column="1"        
                          Grid.Row="3"/>
                <StackPanel       
                        Margin="20 10 0 20"    
                        Orientation="Horizontal"
                        Grid.ColumnSpan="2"
                        Grid.Row="4">
                        <Button x:Name="ButtonAddActor"
                            Command="{Binding AddActorCommand}"
                            Background="#2E4C6D"
                            Content="Add"        
                            Foreground="White"
                            Height="20" 
                            HorizontalAlignment="Center"        
                            Width="100"/>
                        <Button x:Name="ButtonUpdateActor"
                           Command="{Binding UpdateActorCommand}"
                           Background="#2E4C6D"
                           Content="Update"    
                           Foreground="White"
                           Height="20" 
                           HorizontalAlignment="Center"  
                           Margin="10 0 0 0"
                           Width="100"/>
                    </StackPanel>
            </Grid>
        </Border>
    </Grid>
</Window>

Listing 1: XAML for the Screen in figure 2

Run the project, you'll see the following screen. Forget about the data that we are going to add from ViewModel next.

Figure 2: WPF app to Add and Update collection

We need Actor's class, following snippet is for class Actor

internal class Actor
{
    public int ID { get; set; }
    public string? Name { get; set; }
    public DateTime DOB { get; set; }
    public int Age { get; set; }       
    public bool WonOscar { get; set; }
}

Listing 2: class Actor

Approach 1: List

In ViewModel, we need to create List of Actors.

private List<Actor> _listOfActors; 
public List<Actor> ListOfActors
{
    get { return _listOfActors; }
    set { SetProperty(ref _listOfActors, value); }
}

Listing 3: List of Actors

For the Add button, we need the following AddActor(), This method takes the binding variables data and creates a new Actor object which is added to the list of Actors in listing 3.

private void AddActor()
{
    ListOfActors.Add(new Actor()
    {
        ID = 4,
        Name = ActorName,
        DOB = ActorDOB,
        Age = ActorAge,
        WonOscar = ActorWonOscar
    });
}

Listing 4: Add button's logic

On clicking the Update button, We are just updating the name of the actor, Just to prove the point that ObservableCollection's propertyChanged event gets triggered. 

private void UpdateActor()
{
   ListOfActors.FirstOrDefault(act => act.Name == ActorName).Age = ActorAge;
}

Listing 5: Update button's logic

Listing 6 is the overall ViewModel, With dummy data, commands and other binding properties.

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;

namespace ObservableCollectionVsList
{
    internal class MainWindowViewModel : BindableBase
    {
        #region Properties
        private List<Actor> _listOfActors; 
        public List<Actor> ListOfActors
        {
            get { return _listOfActors; }
            set { SetProperty(ref _listOfActors, value); }
        }

        public string? ActorName { get; set; }
        public DateTime ActorDOB { get; set; }
        public int ActorAge { get; set; }
        public bool ActorWonOscar { get; set; }
        #endregion

        #region ICommands  
        public ICommand AddActorCommand { get; set; }
        public ICommand UpdateActorCommand { get; set; }
        #endregion

        #region Constructor
        public MainWindowViewModel()
        {
            LoadCollectionData();
            AddActorCommand = new DelegateCommand(AddActor);
            UpdateActorCommand = new DelegateCommand(UpdateActor);
        }
        #endregion


        private void LoadCollectionData()
        {
            ListOfActors = new List<Actor>
            {
                new Actor()
                {
                    ID = 1,
                    Name = "Johnny Depp",
                    DOB = new DateTime(1963, 6, 9),
                    Age = 58,
                    WonOscar = false
                },

                new Actor()
                {
                    ID = 2,
                    Name = "Leonardo DiCaprio",
                    DOB = new DateTime(1974, 11, 11),
                    Age = 46,
                    WonOscar = true
                },

                new Actor()
                {
                    ID = 3,
                    Name = "Robert Downey Jr.",
                    DOB = new DateTime(1965, 4, 4),
                    Age = 56,
                    WonOscar = false
                }
            };
        }

        private void AddActor()
        {
            ListOfActors.Add(new Actor()
            {
                ID = 4,
                Name = ActorName,
                DOB = ActorDOB,
                Age = ActorAge,
                WonOscar = ActorWonOscar
            });
        }

        private void UpdateActor()
        {
           ListOfActors.FirstOrDefault(act => act.Name == ActorName).Age = ActorAge;
        }
    }
}

Listing 6: ViewModel for MainWindow

Now let's begin with the magic. When you run the project for the first time, you see the output as figure 3. At the top, you'd see the dummy details that are coming from LoadCollectionData(), and in the form below we are entering details of Meryl Streep. 

Figure 3: Adding new actor details

Once you click on an Add Button, In figure 4, you would see List is getting updated behind the scene but unable to update the UI. Well that's a flaw, INotifyCollectionChanged is not working, Let's see if INotifyPropertyChanged is working or not.

Figure 4: New details added to the list of object

In Figure 5, we are changing Johnny depp's age and clicking the update button, as you can notice no change has been reflected in the list.

Figure 5: Updating Johnny Depp's age but ListView is not reflecting the changes

But the list has been updated behind the scenes, as you can see in figure 6. So technically INotifyPropertyChanged is not working either. This is because of the reasons mentioned above.

Figure 6: Johnny Depp's age has been updated in the object

Approach 2: ObservableCollection

Let's make necessary changes, Update the code in viewmodel, change list to ObservableCollection.

private ObservableCollection<Actor> _listOfActors; 
public ObservableCollection<Actor> ListOfActors
{
    get { return _listOfActors; }
    set { SetProperty(ref _listOfActors, value); }
}

Listing 7: ObservableCollection of Actors

Note: there is one more reference in the LoadCollectionData() method, change that to ObservableCollection as well.

Now let's run the project and add a few details. In figure 7, you can see as soon as new actor's details have been added to the list, The ListBox is automatically updated. This is because ObservableCollection uses INotifyCollectionChanged event to trigger the source that list has been modified. 

Figure 7: Adding new record with ObservableCollection and ListView has added a new record

Now let's update the age of Meryl Streep from 72 to 70. You need to update the Actor class to reflect propertychanged events. Code snippet 8 shows how to achieve this. We are using the BindableBase class which implements INotifyPropertyChanged interface. We are only updating the Age property for now, because in our UpdateActor() we are only updating the age of an actor.

internal class Actor : BindableBase
{
    public int ID { get; set; }
    public string? Name { get; set; }
    public DateTime DOB { get; set; }

    private int _age;
    public int Age
    {
        get { return _age; }
        set { SetProperty(ref _age, value); }
    }
    public bool WonOscar { get; set; }
}

Listing 8: Actor class with BindableBase: INotifyPropertyChanged

Changing Age, In figure 8 you would see it has successfully updated the age of Meryl Streep and that has also been notified to UI, this is because of the PropertyChanged event.

Figure 7: Updating age of Meryl Streep with ObservableCollection and ListView is reflecting these changes

Summary

Today we learned the difference between list and ObservableCollection through an example. We saw how to create a WPF application to understand the INotifyCollectionChanged and INotifyPropertyChanged

Find attached source code for reference. Hope this article helps you to understand both list and ObservableCollection.

Happy Coding.


Similar Articles