Notify item changes in observable collection in WPF/C#

Viewed 2091

In my WPF project, i have a Service EF model defined as

public class Service
{
  public int ID
  public string Name
  public decimal Price
}

and in my viewmodel i have.

public class ReceiptViewModel : BindableBase
{
    private ObservableCollection<Service> _services;
    public ObservableCollection<Service> Services
    {
        get { return _services; }
        set { SetProperty(ref _services, value, () => RaisePropertyChanged(nameof(Total))); }
    }

    public decimal Total => Services.Sum(s => s.Price);
}

Total is bound to a textblock in my view and and my observable collection is bound to an itemscontrol with textbox inside.

i want my Total textblock to change everytime the user change one of the price in my collection from the UI. how can i achieve that?

4 Answers

You should implement it yourself, my example of observable collection (and also you need to subscribe and Raise OnPropertyChanged(nameof(Total))) when collection item was changed, or change my implementation of collectionEx to raising collection changed event.

    public class ObservableCollectionEx<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
  public ObservableCollectionEx(IEnumerable<T> initialData) : base(initialData)
  {
      Init();
  }

  public ObservableCollectionEx()
  {
      Init();
  }

  private void Init()
  {
      foreach (T item in Items)
         item.PropertyChanged += ItemOnPropertyChanged;

      CollectionChanged += FullObservableCollectionCollectionChanged;
  }

  private void FullObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  {
      if (e.NewItems != null)
      {
         foreach (T item in e.NewItems)
         {
            if (item != null)
               item.PropertyChanged += ItemOnPropertyChanged;
         }
      }

      if (e.OldItems != null)
      {
          foreach (T item in e.OldItems)
          {
              if (item != null)
                  item.PropertyChanged -= ItemOnPropertyChanged;
          }
      }
  }

    private void ItemOnPropertyChanged(object sender, PropertyChangedEventArgs e)
        => ItemChanged?.Invoke(sender, e);

    public event PropertyChangedEventHandler ItemChanged;
}

I tried to follow @Haukinger and @Bandook advices and i made this

The Model

public class Service
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

The View model that extends the model

public class ServiceViewModel : Service, INotifyPropertyChanged
{
    private decimal price;
    public event PropertyChangedEventHandler PriceChanged;
    public event PropertyChangedEventHandler PropertyChanged;

    public ServiceViewModel()
    {
    }

    public ServiceViewModel(decimal value)
    {
        this.price = value;
    }

    public new decimal Price
    {
        get { return price; }
        set
        {
            price = value;
            // Call OnPropertyChanged whenever the property is updated
            OnPropertyChanged();
        }
    }

    // Create the OnPropertyChanged method to raise the event
    // The calling member's name will be used as the parameter.
    protected void OnPropertyChanged([CallerMemberName] string price = null)
    {
        PriceChanged?.Invoke(this, new PropertyChangedEventArgs(price));
    }
}

The Main View Model

public class MainWindowViewModel : BindableBase
{
    private string _title = "Prism Application";
    public string Title
    {
        get { return _title; }
        set { SetProperty(ref _title, value); }
    }

    private string _firstName = "TABET AOUL";
    public string FirstName
    {
        get { return _firstName; }
        set { SetProperty(ref _firstName, value, ()=> RaisePropertyChanged(nameof(FullName))); }
    }

    private string _lastName = "Abdelkrim";
    public string LastName
    {
        get { return _lastName; }
        set { SetProperty(ref _lastName, value, () => RaisePropertyChanged(nameof(FullName))); }
    }

    public string FullName => $"{FirstName} {LastName}";

    private ObservableCollection<ServiceViewModel> _services = new ObservableCollection<ServiceViewModel>();
    public ObservableCollection<ServiceViewModel> Services
    {
        get { return _services; }
        set { SetProperty(ref _services, value, () => RaisePropertyChanged(nameof(Total))); }
    }

    public decimal Total => Services.Sum(s => s.Price);

    public DelegateCommand AddServiceCommand { get; set; }

    public void AddService()
    {
        var item = new ServiceViewModel { Name = "Service", Price = 100 };
        item.PriceChanged += Item_PriceChanged;
        Services.Add(item);
        RaisePropertyChanged(nameof(Total));
    }

    public MainWindowViewModel()
    {
        var item = new ServiceViewModel { Name = "Service", Price = 100 };
        item.PriceChanged += Item_PriceChanged;
        Services.Add(item);
        AddServiceCommand = new DelegateCommand(new Action(AddService));
    }

    private void Item_PriceChanged(object sender, PropertyChangedEventArgs e)
    {
        RaisePropertyChanged(nameof(Total));
    }
}

The Code seems to work fine, when i change the price of one of the collection item, the Total change in my UI except when i add new item, so i called RaisePropertyChanged manually on clickcommand.

Anyone who have a more elegant solution is welcome

This is straightforward, but tedious:

  1. Create a ServiceViewModel that implements INotifyPropertyChanged
  2. Map the collection from the database to the observable collection of view models (ReceiptViewModel.Services)
  3. Listen to all changes of the observable collection and attach to PropertyChanged of all added items (and detach from all removed items, also attach all items initially present)
  4. Whenever one of the ServiceViewModels changes, set the new ReceiptViewModel.Total (or raise ReceiptViewModel.PropertyChanged if you make that a computed property)
  5. Also update the database when a ServiceViewModel or the collection of view models change

Step 3 may or may not be easier/simpler/more obvious if you use the EventAggregator instead.

Your question is not very clear - what happens with your current implementation? Any errors? Any breakpoint analysis we should be aware of? How are your bindings implemented in the Xaml

Until further info provided, here is a generic answer with what you have provided.

I would make the following changes to your ReceiptViewModel, taking full advantage of Prism MVVM.

public class ReceiptViewModel : BindableBase
{
    private ObservableCollection<Service> _services;
    public ObservableCollection<Service> Services
    {
        get { return _services; }
        set { SetProperty(ref _services, value); }
    }

    private decimal _total;
    public decimal Total
    {
        get { return _total; }
        set { SetProperty(ref _total, Services.Sum(s => s.Price)); }
    }

    public DelegateCommand PriceChangedCommand {get; set;}
    
    public ReceiptViewModel()
    {
       PriceChangedCommand = new DelegateCommand(OnPriceChanged);
    }

    private void OnPriceChanged()
    {
       Total = Services.Sum(s=>s.Price);
    }
}

Now in your XAML (not sure what it looks like currently) so this is an example

<!--Ensure your DataContext / ItemsSource is set correctly-->    
<TextBox Text={Binding Price}>
   <b:InteractionTriggers>
      <b:EventTrigger EventName="TextChanged">
          <prism:InvokeCommandAction Command={Binding DataContext.PriceChangedCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"/>
          <!--Your AncestorType will be your root (top level) e.g. Window/UserControl etc-->
      </b:EventTrigger>
   </b:InteractionTriggers>
</TextBox>

You may not need the RelativeSource binding, as I said - I do not know your current implementation hence I have given a more complex answer that will work in a larger scenario. but you can try the simpler version -

<prism:InvokeCommandAction Command={Binding PriceChangedCommand} />

To add such Behavior to xaml, you might need to use "Blend for Visual Studio" (should be already installed with Visual Studio). Or try to do it without Blend by adding the following namespace in your Window/Usercontrol

xmlns:b="http://schemas.microsoft.com/xaml/behaviors"

And installing the following Nuget Package - Microsoft.Xaml.Behaviors.Wpf

Related