Edit level mismatch in AcceptChanges when moving items from one list to anotherEdit 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