Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested ObservableCollection - Propogate notification from child to parent

I'm developing a WPF application with MVVM Light Toolkit. I just want to display a nested Observablecollection which hold the Employee Attendance details into a DataGrid and do some CRUD functionality in the inner grid and based on those changes I have to automatically recalculate the Outer collection record. The inner collection (PunchDetailModels) is showing in the RowDetailsTemplate of the DataGrid.

Here is the Models :

    public class AttendanceModel : ObservableObject
     {
        public const string EmpNamePropertyName = "EmpName";

        private string _empName = string.Empty;

        public string EmpName
        {
            get
            {
                return _empName;
            }
            set
            {
                Set(EmpNamePropertyName, ref _empName, value);
            }
        }

        public const string PunchDetailModelsPropertyName = "PunchDetailModels";

        private ObservableCollection<PunchDetailModel> _punchDetailModels = null;

        public ObservableCollection<PunchDetailModel> PunchDetailModels
        {
            get
            {
                return _punchDetailModels;
            }
            set
            {
                Set(PunchDetailModelsPropertyName, ref _punchDetailModels, value);
            }
        }           
        private string _inOutCount;
        public string InOutCount
        {
                get
                {
                    return PunchDetailModels != null
                        ? string.Format("{0}/{1}", PunchDetailModels.Count(i => i.PunchStatus == Enums.PunchType.CheckIn),
                            PunchDetailModels.Count(i => i.PunchStatus == Enums.PunchType.CheckOut))
                        : null;
                }
            }

        public TimeSpan? FirstCheckIn
        {
            get
            {
                if (_punchDetailModels != null)
                {
                    var firstCheckIn =
                        _punchDetailModels.OrderBy(t => t.PunchTime)
                            .FirstOrDefault(i => i.PunchStatus == Enums.PunchType.CheckIn);

                    if (firstCheckIn != null)
                        return firstCheckIn.PunchTime;
                }

                return null;
            }
        }


        public TimeSpan? LastCheckOut
        {
            get
            {
                if (_punchDetailModels != null)
                {
                    var lastCheckOut =
                        _punchDetailModels.OrderBy(t => t.PunchTime)
                            .LastOrDefault(o => o.PunchStatus == Enums.PunchType.CheckOut);
                    if (lastCheckOut != null)
                        return lastCheckOut.PunchTime;
                }

                return null;
            }
        }


        public TimeSpan? TotalInTime
        {
            get
            {
                TimeSpan totalInTime = TimeSpan.Zero;

                if (_punchDetailModels != null)
                {
                    if (!IsValidRecord()) return null;

                    for (int inTime = 0; inTime < _punchDetailModels.Count; inTime += 2)
                    {
                        totalInTime += _punchDetailModels[inTime + 1].PunchTime - _punchDetailModels[inTime].PunchTime;
                    }
                }

                return totalInTime;
            }
        }

        public TimeSpan? TotalOutTime
        {
            get
            {
                TimeSpan totalInTime = TimeSpan.Zero;

                if (_punchDetailModels != null)
                {
                    if (!IsValidRecord()) return null;

                    for (int inTime = 1; inTime < _punchDetailModels.Count - 1; inTime += 2)
                    {
                        totalInTime += _punchDetailModels[inTime + 1].PunchTime - _punchDetailModels[inTime].PunchTime;
                    }
                }

                return totalInTime;
            }
        }    
}

public class PunchDetailModel : ObservableObject
    {
        public const string PunchStatusPropertyName = "PunchStatus";

        private Enums.PunchType _punchStatus;

        public Enums.PunchType PunchStatus
        {
            get
            {
                return _punchStatus;
            }
            set
            {
                Set(PunchStatusPropertyName, ref _punchStatus, value);
            }
        }

        public const string PunchTimePropertyName = "PunchTime";

        private TimeSpan _punchTime = TimeSpan.Zero;

        public TimeSpan PunchTime
        {
            get
            {
                return _punchTime;
            }
            set
            {
                Set(PunchTimePropertyName, ref _punchTime, value);
            }
        }

    }

ViewModel :

public const string AttendanceCollectionPropertyName = "AttendanceCollection";

    private ObservableCollection<AttendanceModel> _attendanceCollection = null;
    public ObservableCollection<AttendanceModel> AttendanceCollection
    {
        get
        {
            if (_attendanceCollection == null)
            {
                _attendanceCollection = new ObservableCollection<AttendanceModel>();
                //_attendanceCollection.CollectionChanged+=_attendanceCollection_CollectionChanged;
            }
            return _attendanceCollection;
        }
        set
        {
            Set(AttendanceCollectionPropertyName, ref _attendanceCollection, value);
        }
}

View : enter image description here

Issues I'm facing :

1) When a user ADD or DELETE a particular record from Inner DataGrid, I need to get notification in the View Model. I know it's possible by registering a collection changed event for an ObservableCollection. But how it's possible for an inner ObservableCollection ?

2) I need to get notifications in the viewmodel for any change in CheckIn or Checkout field in the Inner DataGrid, so that I can recalucate fields like TotalInTime, TotalOutTime etc.

How can I do this ? I'm currently stuck with this scenario. Please suggest your valuable points.

like image 506
Dennis Jose Avatar asked Feb 13 '14 05:02

Dennis Jose


1 Answers

I'm guessing that the ObservableObject class is your own implementation of INotifyPropertyChanged interface. Now to solve your issues:

  1. You should register to CollectionChanged event in _punchDetailModels and raise a PropertyChanged event for that variable in the handler, like so:

     public ObservableCollection<PunchDetailModel> PunchDetailModels
    {
      get
      {
        return _punchDetailModels;
      }
      set
      {
        Set(PunchDetailModelsPropertyName, ref _punchDetailModels, value);
         _punchDetailModels.CollectionChanged += handler;
      }
     }           
      private void handler(object sender, NotifyCollectionChangedEventArgs e)
      {
        base.RaisePropertyChanged(PunchDetailModelsPropertyName); // If you don't have a method with such signature in ObservableObject (one that takes a string and raises PropertyChanged for it) you'll have to write it.
       }
    

This way the view should reload automatically when adding or removing elements from the inner collection.

  1. There is no other way than to subscribe to listen to PropertyChanged on these fields. That's what the View does and that's what the ViewModel should do also. Like so:

     public const string AttendanceCollectionPropertyName = "AttendanceCollection";
    
     private ObservableCollection<AttendanceModel> _attendanceCollection = null;
     public ObservableCollection<AttendanceModel> AttendanceCollection
      {
       get
       {
        if (_attendanceCollection == null)
        {
            _attendanceCollection = new ObservableCollection<AttendanceModel>();
        }
        return _attendanceCollection;
      }
       set
      {
        Set(AttendanceCollectionPropertyName, ref _attendanceCollection, value);
        _attendanceCollection.CollectionChanged+= handler
      }
    } 
    
     private void handler(object sender, NotifyCollectionChangedEventArgs e)
     {
      foreach (AttendanceModel model in AttendanceCollection)
            model.PropertyChanged += somethingChanged;
      }
    
      // Very ineffective to subscribe to all elements every time a list changes but I leave optimization to you.
     private somethingChanged (object obj, PropertyChangedEventArgs args)
     {
       if ( args.PropertyName == "CheckIn" ) // for example
        { 
              AttendanceModel ModelToRecalculate = obj as AttendanceModel;
              // You can do anything you want on that model.
        }
     }
    

And of course you need to raise PropertyChanged with string argument of value CheckIn in the AttendanceModel class when You think it's necessary ( for example in the handler method)

EDIT:

To answer your comment question:

"Come to second one - I need to recalculate the Attendance Model properties like InOutCount, TotalInTime, TotalOutTime on PunchTime field update."

The answer is: You don't need to do anything in the ViewModel to "recalculate". The UI is subscribed to PropertyChangefor InOutCount , FirstCheckIn ... and so on. It's beacause of Binding (it does it automatically).

So All you need to do to inform the UI that given model needs to be recalculated is call RaisePropertyChanged("InOutCount"), RaisePropertyChanged("FirstCheckIn"). The UI will understand that it needs to GET these properties and because you have these calcualations in property getters, it'll get recalculated.

So, I see that UI needs to be recalculated every time that the INNER list changes, so all you need to do is change the handler code to CollectionChanged for PunchDetailModels like this:

// the handler for CollectionChanged for the INNER collection (PunchDetailModels)
private void handler(object sender, NotifyCollectionChangedEventArgs e)
      {
        base.RaisePropertyChanged(PunchDetailModelsPropertyName); // If you don't have a method with such signature in ObservableObject (one that takes a string and raises PropertyChanged for it) you'll have to write it.
        base.RaisePropertyChanged("InOutCount")
        base.RaisePropertyChanged("FirstCheckIn")
        base.RaisePropertyChanged("LastCheckOut")
        // and so on for all the properties that need to be refreshed
       }
like image 81
michal.ciurus Avatar answered Oct 01 '22 05:10

michal.ciurus