Vibrant discussion about CSLA .NET and using the framework to build great business applications.
Can anyone tell me where I am going wrong please...?
My problem relates to WinForms databinding and its relationship with a CSLA business object. In fact its CSLA business objects that in turn contain child collections that are really causing me trouble.
I fully understand about the need to disconnect the BO from the UI at the point I wish to cancel or revert changes. There are lots of examples in Rocky's book to demonstrate this. However to make for a pleasing user experience I need to invoke the following functionality.
1). Load the appropriate business object ( and its child collections ). The form obviously contains parent and child bindingsources.
2). Begin the edit.
3). Let the user make as may changes as they like to the BO and any of its child members.
4). Allow the user to save or revert ALL the changes made since the start of the edit session on the current BO. ( so I only want to be able to save/rollback at the parent level )
The only way that I can make this scenario work is to set DisableIEditableObject to true on both the parent and the child business objects. That way I have complete control over when BeginEdit(), ApplyEdit(), or CancelEdit() are called. If I dont do this for some reason the child edit levels get out of sync with the parent ( caused during child navigation via a datagrid no doubt ), and when I call CancelEdit() on the parent, it throws an exception.
I know Rocky says that setting DisableIEditableObject is the wrong road to take and he is ( as always ) right, as some of my bound windows components don't seem to update quite properly with this setting enabled. Surely I dont have to start drilling down into the children and calling CancelEdit() on each one of them do I ?
So, how do I control a save / undo process at the parent level only ?
Sorry if this is going over old ground, but I find windows databinding really frustrating!
Thanks,
Nick
Hi,
When a business object/list is bound to a BindingSource the BindongSource (and DataBinding) assumes it has full control over the objects.
What this means is:
1. Startup semantics:
2. Save semantics:
3. Undo semantics
YOU MUST NEVER CALL BeginEdit, ApplyEdit or CancelEdit on an object that is bound to a BindingSource.,
Jonny Bekkum, Norway CslaContrib Coordinator
I totally agree JonnyBee!
The business object is cloned and the bindingsource datasource set to null etc. before I attempt to do anything to the business object itself...
Sorry for not making this clear.
Jonny is right, but it sounds like you've already dug into the Using CSLA .NET 3.0 ebook in some depth and probably have the basic concept.
If you want a top-level Cancel button, you need to manually call BeginEdit on the root object before you bind it to the UI. And then you need to unbind the entire graph from the UI and call either CancelEdit or ApplyEdit on the root to cancel or before saving.
N-level undo will manage the canceling of all changes to the entire object graph - but not automatically. When data binding calls these methods they act differently (specifically to emulate the way a DataSet/DataTable work). So there really is no "n-level" undo with data binding, because data binding only supports one level.
But if you manually call BeginEdit/CancelEdit/AppyEdit on the root (making sure to never do this to a bound object graph) then you actually get n-level undo.
Rocky
Yes Rocky, I have done a lot of reading on the subject, but I still find it confusing!
I do indeed call BeginEdit() on the root before it is bound to the UI. I also unbind the root before I Cancel or Apply the edit.
Because I have a child grid bound to the child bindingsource ( as well as the user editable text boxes ), I assume that windows databinding calls BeginEdit() and ApplyEdit() etc on the children automatically because its designed to commit data as the active row changes.
What I am still slightly confused about is why the edit level between the parent and children is out of sync, and I get the error. It appears that my approach is correct, so I should not get the error.
If I manually call BeginEdit() before I bind, I dont care what windows binding does in between, at the point I unbind and then call CancelEdit(), I should be able to get back to where I started.
Rocky,
All your examples populate a bindingsource with one business object at a time ( other than child collections of course ). If I wanted to use a bindingsource to hold multiple root objects ( say a list of customers ) and then allow a user to edit those one at a time, is this possible ?
In other words if I load a bindingsource with a bunch of customers ( so as to provide a nice easy way of navigating around ) by just adding them one at a time, could I do something like the following when the user navigates to a new customer in the list:-
int position = bindingSourceCustomers.Position;
bindingSourceCustomers.RaiseListChangedEvents = false;
BoCustomer currentCustomer = (BoCustomer)bindingSourceCustomers[position];
bindingSourceCustomers[position] = null; // Unbind
currentCustomer.BeginEdit(); // Start the edit
bindingSourceCustomers[position] = currentCustomer; // Rebind the UI
bindingSourceCustomers.RaiseListChangedEvents = true;
bindingSourceCustomers.ResetCurrentItem();
Sorry if that sounds mad, but what I am trying to do is have a navigation ( i.e. a bound treeview ) that keeps up to date with the changes being made to customer data. By trying to work with one list only this becomes much easier. I dont feel the need for a dedicated CSLA list as this seems overkill.
If this is a bad idea I'll not take it any further...!
Hi Nick,
No - this is not the proper way to handle bind/unbind or interact with a list.
How do you handle bind/unbind of BindingSources?
Is your problem that you get exception with EditLevel mismatch?
Typical problem is that you must unbind in the opposite order of bind and Bind should keep this order:
I use the following helper extension methods in my apps and very rarely have problems with editlevel mismatch.
public static class BindingSourceExtensions { /// <summary> /// Unbinds the binding source and the Data object. Use this Method to safely disconnect the data object from a BindingSource before saving data. /// </summary> /// <param name="source">The source.</param> /// <param name="cancel">if set to <c>true</c> then call CancelEdit else call EndEdit.</param> /// <param name="isRoot">if set to <c>true</c> this BindingSource contains the Root object. Set to <c>false</c> for nested BindingSources</param> public static void Unbind(this BindingSource source, bool cancel, bool isRoot) { IEditableObject current = null; // position may be -1 if bindigsource is already unbound which results in Exception when trying to address current if ((source.DataSource != null) && (source.Position > -1)) { current = source.Current as IEditableObject; } // set Raise list changed to True source.RaiseListChangedEvents = false; // tell currency manager to suspend binding source.SuspendBinding(); if (isRoot) source.DataSource = null; if (current == null) return; if (cancel) { current.CancelEdit(); } else { current.EndEdit(); } } /// <summary> /// Rebinds the binding source. /// </summary> /// <param name="source">The source.</param> /// <param name="data">The data.</param> public static void Rebind(this BindingSource source, object data) { source.Rebind(data, false); } /// <summary> /// Rebinds the binding source. /// </summary> /// <param name="source">The source.</param> /// <param name="data">The data.</param> /// <param name="metadataChanged">if set to <c>true</c> then metadata (object/list type) was changed.</param> public static void Rebind(this BindingSource source, object data, bool metadataChanged) { if (data != null) { source.DataSource = data; } // set Raise list changed to True source.RaiseListChangedEvents = true; // tell currency manager to resume binding source.ResumeBinding(); // Notify UI controls that the dataobject/list was reset - and if metadata was changed source.ResetBindings(metadataChanged); } }
Hmmm, you may be on to something there I think...!
Yes, I'm guilty as charged in that its an EditLevel mismatch exception...!
Fortunately my list example was only hypothetical, BUT, you may well be right regarding the order in which my unbind was happening. I will investigate and report back.
Out of interest do you think that a bindingsource can be used as a suitable container to hold a list of BOroot objects ?
Thanks!
JonnyBee, I'm sure we have moved a step closer, but its stil complaining of an "Edit level mismatch in UndoChanges" error.
This is the code I am using to populate the bindingsource:
// stop the flow of events
this.bindingSourceActivity.RaiseListChangedEvents = false;
this.bindingSourceExpenses.RaiseListChangedEvents = false;
// Grab the object
_activity = BoCustomerCRMActivity.GetByCustomerCRMActivityID(1);
// Begin the edit
_activity.BeginEdit();
// Rebind the UI
this.bindingSourceActivity.DataSource = null;
this.bindingSourceActivity.DataSource = _activity;
this.bindingSourceExpenses.DataSource = this.bindingSourceActivity;
this.bindingSourceActivity.RaiseListChangedEvents = true;
this.bindingSourceExpenses.RaiseListChangedEvents = true;
this.bindingSourceActivity.ResetBindings(false);
this.bindingSourceExpenses.ResetBindings(false);
In this case bindingSourceActivity is the parent, and bindingSourceExpenses in the child. As per earlier in the post I am beginning an edit before I bind the BO so that I can return here when the edit is over.
As for the cancel or apply method ( note that applyChanges is obviously a boolean parameter passed from outside the method ):-
// Disconnect the UI
UnbindBindingSource(bindingSourceExpenses, applyChanges,
false); UnbindBindingSource(bindingSourceActivity, applyChanges,
UnbindBindingSource(bindingSourceActivity, applyChanges,
true);
(applyChanges) _activity.ApplyEdit();
_activity.ApplyEdit();
else
_activity.CancelEdit();
The code in bold is the code which generates the error. If I'm reading Rocky's reply correctly, this should work, but it doesn't.
Any thoughts ?
i'd like to know a little more about your program:
Which version of Csla are you using?
Do you add new items to the child/grandchild list?
You may also look at the RootChildGrandchildWinFormTest project in the samples folder. This project show an implementation of "DumpEditLevels" that you can implement in your objects and helps you to easier determinate which object(s) have Editlevel out of sync.
Hi Jonny,
I currently run CSLA version 3.8.1.
I am also using Infragistics UI components such as grids and treeviews, and in this instance the child collection has both a grid and a treeview associated with its bindingsource so that the user can navigate around the collection.
Yes, the Child and GrandChild lists need to be editable by the user, so they are subject to having rows added and removed from the collections.
Forgive my ignorance, but where is the samples folder ?!
Cheers,
The samples is packaged in own corresponding downloads available on the download page: http://lhotka.net/cslanet/Download.aspx
I'd also recommend the DataBindingFAQ from WindowsClient.net if you are not already familiar with this document.
Is the grid and treeview connected to the same BindingSource?
I'd recommend to investigate which objects are out of sync on EditLevel after unbind.This should give you some clue as to where EditLevel gets out of sync.
Some "classic" problems I have experienced:
Great, got them now thanks...
As it happens, we also appear to have fixed my problem too.
You were definitely correct in that I wasn't unbinding in the right sequence. But, looking at your prompt about the grid binding I realised that I had messed up as I had another grid on the form bound to the wrong bindingsource and I belive this was fouling up the edit levels. Now this has been corrected the problem has gone, and my parent level undo is working!
I presume the DumpEditLevels method is placed in the required BO and then simply called when required ?
For some reason, the line of code:- gc.DumpEditLevels(sb); Shows as an invalid method. Do you know if this was added after version 3.8.1 or am I missing the point ? Cheers, Nick
gc.DumpEditLevels(sb);
Shows as an invalid method. Do you know if this was added after version 3.8.1 or am I missing the point ?
Good to hear that you fixed the problem.
The DumpEditLevels may be implemented by your BOs (not a part of CSLA framework classes nor the GarbageCollector) and would most likely be used after you have done Unbind to discover which object(s) in your object graph are out of sync on EditLevel.
Not much point in calling this method when DataBinding is active as BeginEdit/EndEdit etc is called repeatedly by DataBinding.
Hi Jonny, Thanks for your help. this thread helped me to solve the "Edit Level mismatch error".
I am using the BindingSourceExtensions as told by you and it really worked to unbind and rebind safely.
But, there is another problem that it don't Mark Root as old after save()....When i call save for a New Root. It runs DataPortal_Insert (), and when i again make changes to object and call save, ite again runs dataportal_insert instead of DataPortal_Update....!!
Actually After Save, the Root object property IsNew not sets False..???
My Code is:
#region Binding Methods
public void BindUI()
{
try
_Root.BeginEdit();
RootBindingSource.DataSource =_Root;
OnBinded();
}
catch (Exception ex)
ControlsGlobal.WriteToTrace(ex.GetBaseException().Message, true); ;
protected void RebindUI(bool saveObject, bool rebind)
// unbind the UI
BindingSourceExtensions.Unbind(this.RootLineBindingSource, !saveObject, false);
BindingSourceExtensions.Unbind(this.RootBindingSource, !saveObject, true);
this.RootLineBindingSource.DataSource = this.RootBindingSource;
// save or cancel changes
if (saveObject)
_Root.ApplyEdit();
if (_AddCharge.IsValid)
OnSaving();
_Root.Save();
ControlsGlobal.WriteToTrace("Record Saved.", true);
this.errorProvider1.BlinkStyle = ErrorBlinkStyle.NeverBlink;
OnSaved();
this.errorProvider1.BlinkStyle = ErrorBlinkStyle.AlwaysBlink;
_Root.CancelEdit();
finally
// rebind UI if requested
if (rebind)
BindUI();
// refresh the UI if rebinding
BindingSourceExtensions.Rebind(RootBindingSource,_Root, false);
BindingSourceExtensions.Rebind(RootLineBindingSource,RootBindingSource, false);
#endregion