7

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.

Dennis Jose
  • 1,589
  • 15
  • 35

1 Answers1

5

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
       }
michal.ciurus
  • 3,616
  • 17
  • 32
  • Attendance Collection Changed Event fire only on outer collection ADD or REMOVE. It won't fire on any property change. So its not possible to raise the property changed event. – Dennis Jose Feb 13 '14 at 11:06
  • 1
    Yeah, OUTER collection doesn't fire `PropertyChanged` other than for itself. I don't see what the problem is. - You wanted the INNER(PunchDetailModels) collection to be refreshed, DONE in point nr 1. Just call `PropertyChanged` for that collection when it changes. - You wanted to know when some INNER fields change. Done in point nr 2. You need to subscribe to `PropertyChanged` of every `AttendanceModel` object and respond to it. – michal.ciurus Feb 13 '14 at 11:14
  • I think I see where the confusion is. The `ViewModel` Listens for `CollectionChanged` event on `AttendanceCollection` to SUBSCRIBE to it's `AttendanceModels` `PropertyChanged` not to RAISE it. – michal.ciurus Feb 13 '14 at 11:22
  • And you may have missed the last lines: "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)" I dodn't write code for this because I thought you could do it yourself. Just fire `PropertyChanged` event with appropriate arguments in the right places in the `AttendanceModel` – michal.ciurus Feb 13 '14 at 11:39
  • The first part is ok. Come to second one - I need to recalculate the Attendance Model properties like InOutCount, TotalInTime, TotalOutTime on PunchTime field update. The PunchTime field is coming from the PunchDetailModels collection. So how should I know which AttendanceModel record has to be changed for a particular upate? – Dennis Jose Feb 13 '14 at 11:51
  • The updated handler will invoke on InnerCollection Collection Changed Event. Right? As you know it will invoke only on ADD or REMOVE. So how can I recalculate the outer collection records on inner collection property update? hope you understand my question.. – Dennis Jose Feb 13 '14 at 12:15
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/47417/discussion-between-dennis-and-mikewazowsky) – Dennis Jose Feb 13 '14 at 12:16