-2

I've implemented deep cloning of ObservableCollection in order to reset items to It's original state in editable Datagrid, via cancel button.

For this I have two collections - one ObservableCollection to bind Datagrid to It, and cloned List to re-initialize ObservableCollection to It's original state when needed.

My code works only first time I hit a cancel button, after that my cloned List has changes in It too.

Provided code is an example (mine is a bit longer), but It's 100% same as mine:

Model, which implements ICloneable:

public class EmployeeModel : ICloneable
{
   public object Clone()
   {
       return MemberwiseClone();
   }
    
   public string NAME
   {
      get { return _name; }
      set
         {
            if (_name != value)
            {
               CHANGE = true;
                _name = value;
             }
          }
     }
     private string _name;

     public string SURNAME
     {
        get { return _surname; }
        set
         {
            if (_surname != value)
            {
               CHANGE = true;
                _surname = value;
             }
          }
     }
     private string _surname; 

     ///<summary>Property for tracking changes in model</summary>
     public bool CHANGE { get; set; }
}

Viewmodel:

public ViewModel() : Base //Implements InotifyPropertyChanged
{
   public ViewModel()
   {
      Task.Run(()=> GetData());
   }

   public ObservableCollection<EmployeeModel> Employees
   {
       get { return _employees; }
       set { _employees = value; OnPropertyChanged();}
   }
   private ObservableCollection<EmployeeModel> _employees;

   public List<EmployeeModel> Copy_employees
   {
        get { return _copy_employees; }
        set { _copy_employees = value; OnPropertyChanged();}
   }
   private List<EmployeeModel> _copy_employees;

   //Fetch data from DB
   private async Task Get_data()
   {
       //Returns new ObservableCollection of type Employee
       Employees = await _procedures.Get_employees();

       if (Employees != null) //Now make a deep copy of Collection
       {
            Copy_employees = new List<EmployeeModel>();
            Copy_employees = Employees.Select(s => (EmployeeModel)s.Clone()).ToList();
       }
    }

   //My Command for canceling changes (reseting DataGrid)
   //CanExecute happens, when model is changed - tracking via CHANGE property of EmployeeModel
   public void Cancel_Execute(object parameter)
   {
        Employees.Clear(); //Tried with re-initializing too, but same result
        
        foreach (var item in Copy_employees)// Reset binded ObservableCollection with old items
        {
             Employees.Add(item);
        }
         
        //Check if copied List really hasn't got any changes                    
        foreach (EmployeeModel item in Copy_employees)
        {
           Console.WriteLine("Changes are " + item.CHANGES.ToString());
        }

     }
 }

Output of cancel command:

1.) First time I hit cancel button:

// Changes are False

Every next time:

// Changes are True

So, as I see It from Console, my copied List get's updated when ObservableColection get's updated, even if It's not binded to DataGrid. And It updates only a property which I changed, so List reflects ObservableCollection items.

How can I keep my original items of List<Employee>, and copy those into binded ObservableCollection anytime ?

Lucy82
  • 654
  • 2
  • 12
  • 32
  • Well not using memberwiseclone. That copies a reference to objects. Take a look at. .https://stackoverflow.com/questions/78536/deep-cloning-objects personally though, i usually use automapper to copy dto into viewmodels. – Andy Apr 20 '21 at 18:28
  • 1
    @Andy: the code shown includes only immutable fields in the `EmployeeModel` object, so while in general you're right that `MemberwiseClone()` isn't going to do a deep copy, in practice that doesn't matter here. – Peter Duniho Apr 20 '21 at 18:41
  • 1
    The code you posted doesn't show any sort of `ToString()` override for `EmployeeModel`, so it's impossible that the code could output any text that looks like `"Changes are False"`. Lacking a proper [mcve], it's impossible to know what's actually going on, and so no good answer can be provided to the question. – Peter Duniho Apr 20 '21 at 18:43
  • @PeterDuniho, I just edited It, didnt' see that, sorry. – Lucy82 Apr 20 '21 at 18:46
  • @Andy, I've tried accepted answer in link you provided, but It doesn't compile in C#. – Lucy82 Apr 20 '21 at 18:47
  • @Andy, I tried accepted answer. Results are still same for me. So I'm back on where I started. Bump. – Lucy82 Apr 20 '21 at 19:04
  • @Lucy82: Are you calling `Cancel_Execute` twice without calling `Get_data()` in between or how to reproduce your issue? – mm8 Apr 20 '21 at 19:22
  • @mm8, no. GetData() is called only once. When I enter Datagrid and edit some cells, my cancel button becomes enabled, and then this code performs on click. Button then disables. Then, when go in Datagrid again to edit a cell I cant cancel again - this time copied collection allready has a mark of CHANGE property to True, so my cancel button doesnt disable anymore. Because my CanExecute is ** return Employees.Any(s=> s. CHANGE==true).FirstOrDefault();**. – Lucy82 Apr 20 '21 at 19:48
  • I think I know what my problem is, but still dont have a clue of how to solve It. What I haven't posted (my bad, sorry) is that Viewmodel actually inherits from generic Base, and both properties are actually defined there. So basically - I think - I share instance of same EmployeeModel to those properties, no matter that I initialize them in Viewmodel. Could I be right? Sorry again for not posting that, I though It was irrelevant. I will edit question tommorow. – Lucy82 Apr 20 '21 at 19:53
  • There is no error in the code you show. And explain in more detail. You started editing several times and then click on undo. What state should the collection return to? The original one created on initialization? Or the state before the last edit? – EldHasp Apr 20 '21 at 19:54
  • @EldHasp,original one. And I will do another copy after saving changed in DataGrid. But that should go If I solve this first. – Lucy82 Apr 20 '21 at 19:56
  • @Lucy82: Put a breakpoint in the setter of `CHANGE` to determine when it's set. – mm8 Apr 20 '21 at 19:56
  • @mm8,done that. And it acts a bit strangely. It occurs on setting the property on first time, which is fine, and then it doesnt stop in break point no more. – Lucy82 Apr 20 '21 at 19:58
  • @Lucy82: Set a breakpoint in the setter of `Copy_employees` as well. – mm8 Apr 20 '21 at 19:59

1 Answers1

1

When you return values, you do not return them, but write backing item references to the editable collection. As a result, you have the same instances in both collections. In the simplest case, when you return them, you also need to clone.

public void Cancel_Execute(object parameter) { Employees.Clear(); //Tried with re-initializing too, but same result

    foreach (var item in Copy_employees)// Reset binded ObservableCollection with old items
    {
         Employees.Add((EmployeeModel)item.Clone());
    }
     
    //Check if copied List really hasn't got any changes                    
    foreach (EmployeeModel item in Copy_employees)
    {
       Console.WriteLine("Changes are " + item.CHANGES.ToString());
    }

 }

Not relevant to the question, but I still advise you to use a slightly more user-friendly interface for cloneable:

public interface ICloneable<T> : ICloneable
{
    new T Clone();
}
EldHasp
  • 6,079
  • 2
  • 9
  • 24
  • thank you so much, I wouldn't figure that out in a couple of days. Though I'm still very confused - when I return items I need to clone them again. If they weren't cloned for the first time without item references of editable collection, then why second cloning works ? – Lucy82 Apr 21 '21 at 06:40
  • thanks for interface too. But I decided to drop ICloneable interface and stick with what Andy proposed - a solution for deep cloning of objects using serialization. It's kind of cleaner code at least. Thanks again !!! – Lucy82 Apr 21 '21 at 06:43
  • If you don't clone every time, then you get links to the same instances in different collections. And for correct work, you need two collections with different, albeit equivalent, specimens. – EldHasp Apr 21 '21 at 07:11
  • 1
    Serialization, in this task, is very redundant and unjustified in terms of the required resources. `ICloneableI` and `Cloneable ` implemented via `MemberwiseClone()`are quite enough here. Serialization is used when the cloned object contains references to other mutable instances of the reference type. There are no such references in your entity type `EmployeeModel`. And even if they are, then the type itself must know about it, and it must decide for itself what should be additionally cloned to obtain a deep copy. – EldHasp Apr 21 '21 at 07:20