Search code examples
c#wpfmvvmbindingobservablecollection

ObservableCollection is not notifying to UI automatically in WPF


I'm trying to build my first app based on MVVM pattern.
I still didn't isolate the view from button click actions cause i got stuck on this issue with the ObservableCollection.
These are the classes:

Model

public class Anagrafica
{   
    public int Cod {  get; set; }
    public string RSoc { get; set; }
    public string Via { get; set; }
    public string Cap { get; set; }
    public string Cit {  get; set; }
    public string Pro { get; set; }
    public string Naz {  get; set; }
    public string Piva { get; set; }
    public string Cfis { get; set; }
    public string NTel { get; set; }
    public string NFax { get; set; }
    public string Email { get; set; }
    public string Ntel { get; set; }
    public string Web { get; set; }
    public string CPag { get; set; }
    public string IBAN { get; set; }
    public string Note { get; set; }
}

ModelService

public interface IAnagraficheSVC
{
    ObservableCollection<Anagrafica> Anagrafiche {  get; }

    void CercaAna(int cod);
}
public class AnagraficheSVC:IAnagraficheSVC
{
    private ObservableCollection<Anagrafica> _anagrafiche = new ObservableCollection<Anagrafica>();

    public ObservableCollection<Anagrafica> Anagrafiche => _anagrafiche;
    public void CercaAna(int cod)
    {
        using (OleDbConnection conn = new OleDbConnection($"PROVIDER=Microsoft.Ace.OLEDB.12.0;Data Source= { Properties.Settings.Default.dbdir }")) 
        {
            if (conn == null)
            {
                throw new Exception("Connection String is Null.");
            }

            OleDbCommand query = new OleDbCommand($"SELECT * from Cli WHERE Cod={cod}", conn);
            OleDbDataAdapter anaDA = new OleDbDataAdapter(query);
            DataTable anaDT = new DataTable();
            anaDA.Fill(anaDT);

            foreach (DataRow row in anaDT.Rows)
            {
                Anagrafica a = new Anagrafica();
                a.Cod = (int)row["Cod"];
                a.RSoc = row["Rsoc"].ToString();
                a.Via = row["IVia"].ToString();
                a.Cap= row["ICap"].ToString();
                a.Cit= row["ICit"].ToString();
                a.Pro= row["IPro"].ToString();
                a.Naz= row["KInt"].ToString();
                a.Piva= row["PIva"].ToString();
                a.Cfis= row["CFis"].ToString();
                a.Email = row["Emai"].ToString();
                a.NTel= row["NTel"].ToString();
                a.Web= row["KUrl"].ToString();
                a.CPag= row["CPag"].ToString();
                a.IBAN= row["NBan"].ToString();
                a.Note = row["No01"].ToString();

                _anagrafiche.Add(a);
            }

        }
    }
}

ViewModel

public class AnagraficaVM
{
    private IAnagraficheSVC _anagraficheSVC = null;

    public ObservableCollection<Anagrafica> Anagrafiche => _anagraficheSVC.Anagrafiche;

    public AnagraficaVM(IAnagraficheSVC anagraficheSVC)
    {
        _anagraficheSVC=anagraficheSVC;
    }

    public void cercaAna(int cod)
    {
        _anagraficheSVC.CercaAna(cod);
    }
}

View.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow(AnagraficaVM AnaVM)
    {
        InitializeComponent();
        AnaTab.DataContext = AnaVM;
    }

    private void Cercabtn_Click(object sender, RoutedEventArgs e)
    {
        (AnaTab.DataContext as AnagraficaVM).cercaAna(int.Parse(txtCod.Text));
    }
}

View (part of)

<TabItem Header="Anagrafica" Name="AnaTab">
    <Border Name="pagborder" Padding="20">
        <Grid Name="AnaGrid" DataContext="{Binding Anagrafiche}">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
                <RowDefinition Height="25"></RowDefinition>
                <RowDefinition Height="25"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Name="lblCod" Style="{StaticResource cod}" FontSize="16" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Center"></Label>
            <TextBox Name="txtCod"  HorizontalAlignment="Left" VerticalAlignment="Center" Width="50" Margin="130,0,0,0" Height="18" Text="{Binding Cod, UpdateSourceTrigger=Explicit}"/>
            </ToggleButton>
            <Rectangle Grid.Row="1" Grid.ColumnSpan="4" Fill="Transparent" Height="10"/>
            <Label Grid.Row="2" Grid.Column="0" FontSize="14" Background="#576CBC">Dati Anagrafici</Label >
            <Label Grid.Row="3" Grid.Column="0">Ragione Sociale</Label>
            <Label Grid.Row="3" Grid.Column="1">Indirizzo</Label>
            <TextBox Grid.Row="4" Grid.Column="0" Name="RagSoc" Text="{Binding RSoc,UpdateSourceTrigger=Explicit}"/>
            <TextBox Grid.Row="4" Grid.Column="1" Name="Indirizzo" Width="210"  Text="{Binding Via}"/>

App.xaml.cs

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        AnagraficheSVC anagraficheSVC = new AnagraficheSVC();
        AnagraficaVM anaVM = new AnagraficaVM(anagraficheSVC);
        Views.MainWindow mainWindow = new Views.MainWindow(anaVM);
        mainWindow.Show();
    }
}

If i understood correctly an ObservableCollection should notify automatically the UI when an item gets added, removed, replaced.
Thing that is not happening in my case: the textboxes i bound to the ViewModel's ObservableCollection are not getting filled with data (while debugging data is inside the ObservableCollection).
If i add _anagraficheSVC.Anagrafiche.Clear(); before _anagraficheSVC.CercaAna(cod);, the textbox txtCod where i inserted the index i am searching gets cleared (so the .Clear() gets notified i guess?).
I think i am missing something, cause it should work.
Since i'm already taking your time, do you think it is a good implementation of the MVVM pattern? In particular the method call _anagraficheSVC.CercaAna(cod); to populate the ObservableCollection: is it legit?

Thank you very much for your time!

EDIT:
If i add a test ListBox in the same container of the textboxes (AnaGrid) and bind the ItemsSource to Anagrafiche, it's working: everytime i click the button i see a new Models.Anagrafica item added inside the list.
Does DataContext listen to CollectionChanged?

UPDATE and SOLUTION:
Well, i knew it that it would be something really stupid.
By typing {Binding Anagrafiche} i was binding the Grid to the ObservableCollection object, which of course has no properties but only Anagrafica items.
By typing {Binding Anagrafiche[0]} i get the first item with his properties and everything works fine. I don't even need any PropertyChanged event for adding or removing items in list.
Thank you everyone and HeldHasp in particular for trying and helping me.


Solution

  • Model public class Anagrafica

    This is not a model from the "MV* Patterns" group. In these patterns, the Model is the LAYER of the application that contains all the Business (Domain) Logic. Your class Anagrafica is an entity. Possibly DTO. Such classes are called models in ADO. But ADO is the Repository layer, which is part of the implementation of the Application Model.

    ModelService

    This service is in fact a Model in MVVM. Taking into account the fact that you do not have notification of a possible change in the _anagrafiche collection, I advise you to change the implementation in order to avoid any random errors.

    public class AnagraficheSVC:IAnagraficheSVC
    {
        // private ObservableCollection<Anagrafica> _anagrafiche = new ObservableCollection<Anagrafica>();
    
        public ObservableCollection<Anagrafica> Anagrafiche {get;} = new();
    

    View (part of)

    Honestly, I can't say I fully understand what you want to show in the GUI. I'll assume that for each element of the Anagrafiche collection you want to create a tab, and show the element's details in the tabs.

    If I'm guessing right, it should be something like this:

    <TabControl ItemsSource="{Binding Anagrafiche}"
                DisplayMemberPath="Some Property Anagrafica">
        <TabControl.ContentTemplate>
            <DataTemplate DataType="{x:Type model:Anagrafica}">
                <Border Name="pagborder" Padding="20">
                    <Grid Name="AnaGrid">
                        <Grid.RowDefinitions>
                 <!-- Continion XAML code to represent one Anagrafica object -->
        </TabControl.ContentTemplate>
                 <!-- Continuation of the XAML code -->
    </TabControl>
    

    There it is github.com/Tvuce/WPFpj.git

    Here's a working implementation:

        public class User
        {
            public string Name { get; set; } = string.Empty;
            public string Surn { get; set; } = string.Empty;
            public int Age { get; set; }
        }
    
        public class UsersViewModel
        {
            public ObservableCollection<User> Users { get; } = new();
    
            private RelayCommand? _addUser;
            public RelayCommand AddUser => _addUser ??= new(() => Users.Add(new User() { Name = "cane: " + Users.Count, Surn = "lupo", Age = 15 }));
        }
    
    <Window x:Class="testbinding.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:testbinding"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800" Background="DarkBlue">
        <Window.DataContext>
            <local:UsersViewModel/>
        </Window.DataContext>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <TabControl ItemsSource="{Binding Users}" DisplayMemberPath="Name">
                <TabControl.ContentTemplate>
                    <DataTemplate DataType="{x:Type local:User}">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition ></ColumnDefinition>
                                <ColumnDefinition ></ColumnDefinition>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition></RowDefinition>
                                <RowDefinition></RowDefinition>
                                <RowDefinition></RowDefinition>
                                <RowDefinition></RowDefinition>
                            </Grid.RowDefinitions>
                            <TextBox Grid.Row="0" Grid.Column="0"
                                     Height="20" Width="100"
                                     Text="{Binding Name}"/>
                            <TextBox Grid.Row="0" Grid.Column="1"
                                     Height="20" Width="100"
                                     Text="{Binding Surn}"/>
                            <TextBox Grid.Row="1" Grid.Column="0"
                                     Height="20" Width="100"
                                     Text="{Binding Age}"/>
                        </Grid>
                    </DataTemplate>
                </TabControl.ContentTemplate>
            </TabControl>
            <Button Grid.Column="0" Grid.Row="1"
                    Command="{Binding AddUser}"
                    Background="LightGray" Width="100" Height="60">Add User</Button>
        </Grid>
    </Window>
    
        public partial class MainWindow : Window
        {
            // Getting ViewModel in Code Behind from DataContext.
            //In general, it is better not to do this. And leave Code Behind empty.
            private readonly UsersViewModel viewModel;
            public MainWindow()
            {
                InitializeComponent();
    
                viewModel = (UsersViewModel)DataContext;
            }
        }
    

    ...i read textboxes are only bound to PropertyChanged events...

    If you need to display only one element (object), then it is better to use INotifyPropertyChanged.PropertyChanged. But for the sake of example, for the purpose of learning and improving understanding, I will show you how this can be done using ObservableCollection.

        public partial class MainWindow : Window
        {
            public ObservableCollection<User> o;
            public MainWindow()
            {
                InitializeComponent();
                o = new ObservableCollection<User>();
                DataContext = o;
            }
    
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                User p = new User() { name = "cane", surn = "lupo", age = 15 };
                if (o.Count == 0)
                    o.Add(p);
                else
                    o[0] = p;
            }
    
    <Window x:Class="testbinding.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:testbinding"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800" Background="DarkBlue">
        <Grid DataContext="{Binding [0]}">
            <Grid.ColumnDefinitions>
                <ColumnDefinition ></ColumnDefinition>
                <ColumnDefinition ></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition></RowDefinition>
                <RowDefinition></RowDefinition>
                <RowDefinition></RowDefinition>
                <RowDefinition></RowDefinition>
            </Grid.RowDefinitions>
            <TextBox Grid.Row="0" Grid.Column="0" Name="nome" Height="20" Width="100" Text="{Binding name}"></TextBox>
            <TextBox Grid.Row="0" Grid.Column="1" Name="cogn" Height="20" Width="100" Text="{Binding surn}"></TextBox>
            <TextBox Grid.Row="1" Grid.Column="0" Name="eta" Height="20" Width="100" Text="{Binding age}"></TextBox>
            <Button Grid.Column="0" Grid.Row="2" Click="Button_Click" Background="LightGray" Width="100" Height="60">Press</Button>
        </Grid>
    </Window>