I am trying to do something that I thought would be dead easy but it’s proving to be anything but that. I have a simple editable root list object that I want to edit in a data grid view. I want the user to be able to edit the data in the grid, and then click a save or cancel button to save or cancel ALL the changes. The save bit is easy, but I cannot get the cancel to work.
In my simplified test example I have created the BusinessListBase object and a form with a binding source control, a data grid view and a save and cancel button. In the form load I get the list object and assign it to the binding source. In the cancel button click event I call myList.CancelEdit.
I can get it to cancel the update to the last row in the grid, but if I change more than one row it will not cancel the others. I have read through quite a few other posts on this issue and I am more confused than when I started. Is there a simple and reliable way to get the cancel to work or should I give up on it now?
We got this working in a web environment just yesterday. At first, we didn't think it worked, but we were failing to set/get the cached copy of the list at the right times.
We were only going for one level of undo, because in the web environment we're only editing one record at a time (so far. :).
I'm not implementing caching, just trying to do a CancelEdit on a single instance of an object. I also only want one level of undo, but once I edit more than one row in the grid I can't even get one level of undo. I would be interested to hear what you changed to get it working though.
In truth, my object started out with caching (implemented just like in the book using a shared variable), and eventually I need to get back to that. I have removed it at this point as I need to get the Cancel working reliably.
In fact if it weren't for my need to use a cached object I would simply achieve my goal by discarding the object. However, because it does use caching I need to get the Cancel working reliably.
I'm working on this at the moment - doing some research. It doesn't look all that good.
Data binding is very aggressive about calling BeginEdit on the current row. To the point that immediately after you do bindingSource.CancelEdit/EndEdit it calls BeginEdit.
This means that the current row is always one edit level above all other rows. Even unbinding the list leaves that row at an elevated edit level, because they don't do an EndEdit or CancelEdit during the unbinding process.
It looks like this sort of approach may solve the issue:
private void cancelButton_Click(object sender, EventArgs e)
{
// get business object reference
DataList list = (DataList)this.dataListBindingSource.DataSource;
// cancel current row
this.dataListBindingSource.CancelEdit();
// unbind the UI
UnbindBindingSource(this.dataListBindingSource);
// cancel the list and restart editing
list.CancelEdit();
list.BeginEdit();
// rebind the UI
this.dataListBindingSource.DataSource = list;
}
private void UnbindBindingSource(BindingSource source)
{
System.ComponentModel.IEditableObject current = this.dataListBindingSource.Current as System.ComponentModel.IEditableObject;
this.dataListBindingSource.DataSource = null;
if (current != null)
current.EndEdit();
}
This presupposes that you've called BeginEdit() manually on the business list BEFORE binding it to the bindingSource in the first place.
Thanks for the post Rocky, that appears to work well. I've tested it with both the DevEx grid and the DataGridView and it's fine with both.
When you say 'it doesn't look good', do you mean it's not a particularly simple/clear solution, or is there something else that you are concerned about? I get very nervous about including somthing in a commercial application that's fragile.
“Doesn’t look good” merely means that the code
behind a cancel button is a lot more complex than I think it should be. I keep
thinking that this is just not a complex goal, but that code is many times
longer than I’d like to see… (2 lines seems about right to me) But
it is what it is, and this technique does solve the issue.
You should know that this morning’s research did uncover a
somewhat obscure bug in CSLA itself, which is now fixed in svn – both BLB
and BB were impacted in the solution. You could back-port the changes into
2.1.4 though, they aren’t complex.
The bug manifests only in the case that:
1.
A user selects a row
2.
The user deletes THAT row
3.
The user clicks the form-level Cancel
The result is that the deleted-and-now-undeleted row has an
elevated edit level (1 higher than it should) and so you’ll get some
unpredictable results.
One of the things on the wish list is to catch incorrect edit
levels like that and throw an exception. I think that may have to come higher
in the priority list, because it would certainly help debug/diagnose this type
of issue much more rapidly.
Rocky
No virus found in this outgoing message.
Checked by AVG Free Edition.
Version: 7.5.467 / Virus Database: 269.7.3/809 - Release Date: 5/17/2007 5:18 PM
Yes, the issue is resolved in 3.0.2. Though the resolution is mostly in your code, not in CSLA itself, though I did make a couple small (but important) changes to CSLA as well.
The big changes though, are in PTWin, and you need to model your UI code directly after the approach taken in the PTWin forms - which is a fair amount of code, but at least it works.
Thankfully WPF is simpler, and in the long run it'll replace Windows Forms, so this complexity is (kind of) a moot point.
When we started implementing our UI code we were not following the databinding code that you have in PTWin (unknowingly). Once I was beta'ing CSLA 3 and we were getting edit-level exceptions, we've moved all Save(), New(), and Delete() calls on the UI to generic methods that follow your PTWin examples:
public
static T SaveRoot<T>(T businessObject) where T : BusinessBase<T>{
return SaveRoot(businessObject, null, false);}
public static T SaveRoot<T>(T businessObject, BindingSource bindingSource, bool rebindDataSource) where T : BusinessBase<T>{
if (bindingSource != null){
bindingSource.RaiseListChangedEvents =
false;}
T Clone = businessObject.Clone();
Clone.ApplyEdit();
try{
businessObject = Clone.Save();
businessObject.BeginEdit();
if (rebindDataSource){
if (bindingSource != null){
bindingSource.DataSource =
null;bindingSource.DataSource = businessObject;
}
}
}
catch (DataPortalException ex){
MessageBox.Show(ex.BusinessException.ToString(), "Error saving", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);}
catch (Exception ex){
MessageBox.Show(ex.ToString(), "Error Saving", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);}
finally{
if (bindingSource != null){
bindingSource.RaiseListChangedEvents =
true;}
}
return businessObject;}
public static void UndoRoot<T>(T businessObject) where T : BusinessBase<T>{
businessObject.CancelEdit();
}
public static void DeleteRoot<T>(T businessObject) where T : BusinessBase<T>{
try{
businessObject.Delete();
businessObject.ApplyEdit();
businessObject.Save();
}
catch (DataPortalException ex){
MessageBox.Show(ex.BusinessException.ToString(), "Error deleting", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);}
catch (Exception ex){
MessageBox.Show(ex.ToString(), "Error deleting", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);}
}
public static T SaveRootList<T, C>(T businessObject) where T : BusinessListBase<T, C> where C : IEditableBusinessObject{
return SaveRootList<T, C>(businessObject, null, false);}
public static T SaveRootList<T, C>(T businessObject, BindingSource bindingSource, bool rebindDataSource) where T : BusinessListBase<T, C> where C : IEditableBusinessObject{
if (bindingSource != null){
bindingSource.RaiseListChangedEvents =
false;}
T Clone = businessObject.Clone();
Clone.ApplyEdit();
try{
businessObject = Clone.Save();
businessObject.BeginEdit();
if (rebindDataSource){
if (bindingSource != null){
bindingSource.DataSource =
null;bindingSource.DataSource = businessObject;
}
}
}
catch (DataPortalException ex){
MessageBox.Show(ex.BusinessException.ToString(), "Error saving", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);}
catch (Exception ex){
MessageBox.Show(ex.ToString(), "Error Saving", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);}
finally{
if (bindingSource != null){
bindingSource.RaiseListChangedEvents =
true;}
}
return businessObject;}
public static void UndoRootList<T, C>(T businessObject) where T : BusinessListBase<T, C> where C : IEditableBusinessObject{
businessObject.CancelEdit();
businessObject.BeginEdit();
}
I would expect that this will port over correctly to the new framework, but my generic 'UndoRootList()' method will not need the supplementary 'BeginEdit()'... Am I overlooking something?
I don’t have time to look through your code right now, so
I’ll assume it is a correct abstraction of the 3.0.2 code from PTWin. But
make sure it is 3.0.2 – because even 3.0.1 had bugs! This has been
changing fast, and only 3.0.2 appears to be correct.
The challenge is where you have multiple bindingsource controls,
because the order in which they are processed is critical, and you must
differentiate between the root and any child bindingsource controls. Unfortunately,
I didn’t see any way to ask the control whether it is a “root”
or “child”, and I didn’t see any way to abstract this
process, because only the author of a given form knows the relationships
between the different controls…
Rocky
Copyright (c) Marimer LLC