Edit level mismatch in AcceptChanges when moving items from one list to another

Edit level mismatch in AcceptChanges when moving items from one list to another

Old forum URL: forums.lhotka.net/forums/t/8090.aspx


alef posted on Friday, December 04, 2009


In the UI I'm using the following code :

      private void cmdAdd_Click(object sender, EventArgs e)
      {
        _employee.AddCountry(((Country)AvailableCountriesListBox.SelectedItem));
       
      }

The UI has two listboxes and the user can move country objects from the available listbox to the selected listbox. But I'm receiving the following error : Edit level mismatch in AcceptChanges

The code in the business layer:

 public CountryCollection SelectedCountries
    {
      get
      {
        if (!FieldManager.FieldExists(SelectedCountriesProperty))
          LoadProperty(SelectedCountriesProperty, CountryCollection.NewCountryCollection());
        return GetProperty(SelectedCountriesProperty);
      }
    }

    public CountryCollection NonSelectedCountries
    {
      get
      {
        if (!FieldManager.FieldExists(NonSelectedCountriesProperty))
          LoadProperty(NonSelectedCountriesProperty, CountryCollection.NewCountryCollection());
        return GetProperty(NonSelectedCountriesProperty);
      }
    }

    public void AddCountry(Country country)
    {
      NonSelectedCountries.Remove(country);
      SelectedCountries.Add(country);

    }

    public void RemoveCountry(Country country)
    {
      NonSelectedCountries.Add(country);
      SelectedCountries.Remove(country);

    }


When using the following methods I don't receive an error, but here I'm creating new Country objects and copying the value of the property CountryId

    public void AddCountry(ICountry country)
    {
      Country item = SelectedCountries.AddNew();
      item.CountryId = country.CountryId;
      NonSelectedCountries.Remove(country.CountryId);

    }

    public void RemoveCountry(ICountry country)
    {
      SelectedCountries.Remove(country.CountryId);
      Country item = NonSelectedCountries.AddNew();
      item.CountryId = country.CountryId;

    }


What can be the problem?

RockfordLhotka replied on Friday, December 04, 2009

You are probably running into issues/limitations with Windows Forms data binding.

The rule when dealing with Windows Forms data binding is that you can never directly interact with an object while it is data bound. You must always unbind an object before manipulating it (outside just settng properties).

You are removing an item from a list, and adding it to another list. All while both lists and the child object are data bound. This violates the rule.

(to head off possible questions - yes, this is simpler in Silverlight/WPF because data binding is simpler in XAML)

There are various possible solutions. "Cloning" the object into the other list is a valid answer, and is probably the most common solution.

Also, is Country really a child of this parent object? I don't know what the parent object is, but very few things own a Country. A Ruler or Dictator might. Arguably (in the US) a Citizen owns a Country in a very loose sense.

If you get what I mean, it could be the case that the CountryList object should be a read-only list, and your business object should be maintaining two (or one) collection of CountryId values and nothing else - because your object doesn't own the country, it owns its relationship with the country.

alef replied on Friday, December 04, 2009

I'm using the cslaActionExtender control, so normally the unbinding should be done automatically.

But what happens with the EditLevelAdded property? The item Country exists first in the list NonSelectedCountries. So when the user wants to move it to the SelectedCountries list, what happens with the EditLevelAdded property because we are adding the Country object for the second time.

The parent object is an employee.
The whole UCase you can find in the following thread http://forums.lhotka.net/forums/thread/38568.aspx.
I've attached also an example in this thread.
I think you have a point here to say :  "Employee does not own a Country". But this we discuss better in the other thread to keep the issues separately. I'm looking forward to see your solution on this thread also.





RockfordLhotka replied on Friday, December 04, 2009

cslaActionExtender works at a form level. It doesn’t unbind your object while the form is active – and you are moving an item from one editable list to another editable list while the form is active. cslaActionExtender doesn’t automatically make that scenario work.

 

alef replied on Friday, December 04, 2009

   When doing the binding manually with the following it is the same problem.

  private void buttonApply_Click(object sender, EventArgs e)
      {
        RebindUI(true, true);
      }

      private void RebindUI(bool saveObject, bool rebind)
      {
        // disable events
        this.employeeBindingSource.RaiseListChangedEvents = false;
        this.selectedCountriesBindingSource.RaiseListChangedEvents = false;
        this.nonSelectedCountriesBindingSource.RaiseListChangedEvents = false;
        try
        {
          // unbind the UI
          UnbindBindingSource(this.nonSelectedCountriesBindingSource, saveObject, false);
          UnbindBindingSource(this.selectedCountriesBindingSource, saveObject, false);
          UnbindBindingSource(this.employeeBindingSource, saveObject, true);

          // save or cancel changes
          if (saveObject)
          {
            _employee.ApplyEdit();
            try
            {
              _employee = _employee.Save();
            }
            catch (Csla.DataPortalException ex)
            {
              MessageBox.Show(ex.BusinessException.ToString());
            }
            catch (Exception ex)
            {
              MessageBox.Show(ex.ToString());
            }
          }
          else
            _employee.CancelEdit();
        }
        finally
        {
          // rebind UI if requested
          if (rebind)
            BindUI();

          // restore events
          this.employeeBindingSource.RaiseListChangedEvents = true;
          this.selectedCountriesBindingSource.RaiseListChangedEvents = true;
          this.nonSelectedCountriesBindingSource.RaiseListChangedEvents = true;

          if (rebind)
          {
            // refresh the UI if rebinding
            this.employeeBindingSource.ResetBindings(false);
            this.selectedCountriesBindingSource.ResetBindings(false);
            this.nonSelectedCountriesBindingSource.ResetBindings(false);
          }
        }
      }

     private void UnbindBindingSource(BindingSource source, bool apply, bool isRoot)
      {
        System.ComponentModel.IEditableObject current =
          source.Current as System.ComponentModel.IEditableObject;
        if (isRoot)
          source.DataSource = null;
        if (current != null)
          if (apply)
            current.EndEdit();
          else
            current.CancelEdit();
      }

alef replied on Friday, December 04, 2009


When doing the following in a Unit Test it works:
        _employee.BeginEdit();
        _employee.AddCountry(_employee.NonSelectedCountries[0]);
        _employee.ApplyEdit();
        _employee.Save();

So it is definitely the binding in the UI which causes the problem.
The code in my previous reply is only when saving the form.
So maybe when calling the method AddCountry on the _employee object I have to do also something??

      private void cmdAdd_Click(object sender, EventArgs e)
      {
        _employee.AddCountry(((Country)AvailableCountriesListBox.SelectedItem));
       
      }

RockfordLhotka replied on Friday, December 04, 2009

Your unit test probably isn’t simulating data binding though. Odds are you have a bindingsource for each collection? So the current item in each collection (based on UI currency) is also running at an elevated edit level.

 

Additionally, calling BeginEdit() directly is not the same as casting to IEditableObject and then calling BeginEdit() – and data binding only goes through IEditableObject.

 

From: alef [mailto:cslanet@lhotka.net]
Sent: Friday, December 04, 2009 10:10 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: Edit level mismatch in AcceptChanges when moving items from one list to another

 


When doing the following in a Unit Test it works:
        _employee.BeginEdit();
        _employee.AddCountry(_employee.NonSelectedCountries[0]);
        _employee.ApplyEdit();
        _employee.Save();

So it is definitely the binding in the UI which causes the problem.
The code in my previous reply is only when saving the form.
So maybe when calling the method AddCountry on the _employee object I have to do also something??

      private void cmdAdd_Click(object sender, EventArgs e)
      {
        _employee.AddCountry(((Country)AvailableCountriesListBox.SelectedItem));
       
      }


alef replied on Saturday, December 05, 2009

I'm sorry. I my unit test

        _employee.BeginEdit();
        _employee.AddCountry(_employee.NonSelectedCountries[0]);
        _employee.ApplyEdit();
        _employee.Save();

 I tested the method AddCountry (creating a new Country object) which also works in the UI.

    public void AddCountry(ICountry country)
    {
      Country item = SelectedCountries.AddNew();
      item.CountryId = country.CountryId;
      NonSelectedCountries.Remove(country.CountryId);

    }

 

When unit testing (so without binding) the following method (using the same Country object)

    public void AddCountry(Country country)
    {
      NonSelectedCountries.Remove(country);
      SelectedCountries.Add(country);

    }

the Unit Test fails.

So I'm thinking that this case is not taken care of in CSLA (moving items between lists).The country object is linked to two collections. 1) the DeletedList from NonSelectedCountries and 2) the SelectedCountries list.

Below you can find the Edit Level of the objects at the different moments.

EL = EditLevel   ;    ELA = EditLevelAdded

A) Edit level of the objects before calling  NonSelectedCountries.Remove(country);

Empoyee Auman : EL=1

  SelectedCountries

    Country USA EL=1 (ELA = 0)

  NonSelectedCountries

    Country Begium EL=1 (ELA = 0)

    Country UK EL=1 (ELA = 0)

B) Edit level of the objects after calling  NonSelectedCountries.Remove(country);

Empoyee Auman : EL=1

  SelectedCountries

    Country USA EL=1 (ELA = 0)

  NonSelectedCountries

    Country Begium EL=1 (ELA = 0) ==> belongs now  to the internal DeletedList of CSLA

    Country UK EL=1 (ELA = 0)

C) Edit level of the objects after calling  SelectedCountries.Add(country);

Empoyee Auman : EL=1

  SelectedCountries

    Country USA EL=1 (ELA = 0)

    Country Begium EL=1 (ELA = 1)

  NonSelectedCountries

    Country Begium EL=1 (ELA = 1) ==> belongs to the internal DeletedList of CSLA

    Country UK EL=1 (ELA = 0)

 

So as you can see the Country object Belgium belongs to two collections.

So now when calling _employee.ApplyEdit() we get the error Edit level mismatch in AcceptChanges.

I'm thinking when calling ApplyEdit, AcceptChanges will be called with EditLevel-1 and this will loop through the whole hierarchy of objects and so the EditLevel of Country object Belgium will be decreased two times which finally results in an error.

 

 


 

 

 

 

RockfordLhotka replied on Saturday, December 05, 2009

Ahh, I see, I need to take more time when answering some of these questions…

 

You are right, one object can only have one parent. Once an object is a child of one BLB, it is forever the child of that BLB. That’s the way BLB works.

 

Remember that when you delete a child from a BLB, that child just goes into the DeletedList – it is still a child, it just isn’t visible.

 

So you can’t add that child to another BLB, because it already has a parent.

 

You can probably fake this out with a little work. I haven’t tried this, but you could probably implement a method in your collection class like:

 

public void ReallyRemove(ChildType child)

{

  child.BeginEdit();

  Remove(child);

  DeletedList.Remove(child);

  child.CancelEdit();

}

 

This would remove the child from the list, then remove it from DeletedList so the list has no reference to the child.

 

The trick is that the child gets marked for deletion as part of that process, so doing BeginEdit() and CancelEdit() should (I think) reset the IsDeleted value to false, while leaving the child detached from the list.

 

The child’s Parent property will still point to the list – but that’ll get reset when you add it to the other list.

alef replied on Monday, December 07, 2009

Thanks for given this information.

Finally I decided to create a new object and copying the values of the properties.
I prefer this more then implementing the workaround because otherwise the undo will not work anymore when we delete the object in the DeletedList. When the item in the DeletedList is gone the undo can't do his work anymore

The reason I wanted to move the item in place of creating a new item was for the implementation of the GUI control (the duallist). When the user moves an item from left to right, I want that the moved item in the right listbox should be selected for a user friendly GUI. When it was really the same item (ReferenceEquals) in the left and the right listbox it was easy to implement this selection.
Because of the side effects (Edit level mismatch in AcceptChanges) I searched another solution for the GUI control. The solution I implemented was to override the Equals method.

    protected override object GetIdValue()
    {
      return ReadProperty<int?>(CountryIdProperty);
    }

    public override bool Equals(object obj)
    {
      if (obj is Country)
      {
        return Object.Equals(GetIdValue(), ((Country)obj).GetIdValue());
      }
      else
        return false;
    }


Again many thanks. I've now a perfectly working solution.
So lesson learned : do not move items between lists.

Copyright (c) Marimer LLC