-2

In my current WPF project I was working without implementing INotifyPropertyChanged in ViewModels however I ran into a problem where views' datagrids would load before the async functions finished getting the data which resulted in empty Datagrids. It's my first time using WPF and MVVM.

The View Model

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MLaRealERP.Models;
using System.ComponentModel;

namespace MLaRealERP.ViewModels
{
    public class AlmacenesViewModel:INotifyPropertyChanged
    {
        
        public List<Almacen> insumos;
        
        public AlmacenesViewModel()
        {
         
            insumos = new List<Almacen>();
        }
        public async void InitializeViewModel()
        {
            
            insumos = await Funciones.GetAll<Almacen>("Almacens",App.client);
        }
    }
}

The model

namespace MLaRealERP.Models
{
    public class Almacen
    {
        [JsonPropertyName("id")]
        public int Id { get; set; }

        [JsonPropertyName("name")]
        public string Name { get; set; }
       
    }
}

The View

namespace MLaRealERP.Views
{
    /// <summary>
    /// Interaction logic for AlmacenesView.xaml
    /// </summary>
    public partial class AlmacenesView : Window
    {
        AlmacenesViewModel AlmacenesVM;
        public AlmacenesView()
        {
            InitializeComponent();
            AlmacenesVM = new AlmacenesViewModel();
            AlmacenesVM.InitializeViewModel();
            dgInsumos.ItemsSource = AlmacenesVM.insumos;
        }

        //public async void LoadViewItems()
        //{
        //    insumos.InitializeViewModel();
        //}
        
        private void txtFilter_TextChanged(object sender, TextChangedEventArgs e)
        {

        }

        private void btnAddInsumo_Click(object sender, RoutedEventArgs e)
        {
            dgInsumos.ItemsSource = AlmacenesVM.insumos;
        }

        private void btnVerInsumos_Click(object sender, RoutedEventArgs e)
        {

        }
    }
}

The async http request function just in case its relevant

public static class Funciones
    {
        public static async Task<List<T>> GetAll<T>(string s, HttpClient client)
        {
            System.Threading.Tasks.Task<System.IO.Stream> streamTask;
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/json"));
            streamTask = client.GetStreamAsync(s);
            var repositories = await JsonSerializer.DeserializeAsync<List<T>>(await streamTask);
            return repositories;
        }
        public static async Task<T> Get<T>(string s, int i, HttpClient client)
        {
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/json"));
            s = s + "/" + i.ToString();
            try
            {
                var item = await client.GetFromJsonAsync<T>(s);
                return item;
            }
            catch { };
            return default(T);
        }
}
  • 1
    What is your actual question? – Kirk Woll Mar 23 '22 at 01:13
  • `public List insumos;` should be an `ObservableCollection` and `class Almacen` should implement `INotifyPropertyChanged` – rfmodulator Mar 23 '22 at 01:13
  • 1
    `public async void InitializeViewModel()` -- [Avoid async void](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void). This might also be helpful: [Can constructors be async?](https://stackoverflow.com/questions/8145479/can-constructors-be-async) – Theodor Zoulias Mar 23 '22 at 03:46

1 Answers1

1

Your missing INotifyPropertyChanged implementation is not the only problem (missing notification, memory leak). The original issue that the constructor continues before the AlmacenesViewModel.InitializeViewModel has completed is related to the way you call asynchronous methods: you must await them! Now that the main thread is not waiting for the async method to complete and the DataGrid is not binding to a property that raises the PropertyChanged event, the changes are not detected by the control.

Since a constructor can't be async, the constructor is the wrong place to execute async operations.
Usually an operation is async when it blocks the current thread. Such methods should generally not be part of the instance construction. A constructor must always return fast.

The solution is to invoke this async method deferred, e.g. using Lazy<T>, to initialized the corresponding property the moment they are accessed. In case of a control you can also use the Loaded event to defer initialization.

Implementing INotifyPropertyChanged on the binding source, when the mode is not OneTime or OneWayToSource, is essential. Otherwise you will create a memory leak. See this example to learn how to implement it properly: INotifyPropertyChanged.PropertyChanged.

Remember that an async method should must always return a Task or Task<T>. The only exception is if the method is an event handler.

ViewModel.cs

class ViewModel : INotifyPropertyChanged
{
  public async Task InitializeAsync()
  {
    this.DataModels = await GetDataModelsAsync();
  }

  private List<DataModel> dataModels;
  public List<DataModel> DataModels
  {
    get => this.dataModels;
    set
    {
      this.dataModels = value;
      OnPropertyChanged();
    }
  }
    
  public event PropertyChangedEventHandler PropertyChanged;

  // This method is called by the Set accessor of each property.
  // The CallerMemberName attribute that is applied to the optional propertyName
  // parameter causes the property name of the caller to be substituted as an argument.
  private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();
  
    this.DataContext = new ViewModel();
    this.Loaded += OnLoaded;
  }

  private async void OnLoaded(object sender, EventArgs e)
  {
    await (this.DataContext as ViewModel).InitializeAsync();
  }
}

MainWindow.xaml

<Window>
  <DataGrid ItemsSource="{Binding DataModels}" />
</Window>
BionicCode
  • 1
  • 4
  • 28
  • 44