I'm having a problem with a simple EditableRootParent (Invoice) which has an EditableChildList (InvoiceDetails) which is bound to a form with a couple of text boxes for the root and a DataGridView or devExoress for the children.
The purpose is to calculate the total for the Invoice, when adding or modifying InvoiceDetails in the grid.
The problem is that when adding a new line in the XtraGrid, the new item row loses its focus.
When you put the code OnPropertyChanged("Total"); from the method ReclacTotal from the class Invoice in comment the grid from DevExpress behaves as expected, but then the Total will not be updated on the form.
class Invoice
private void RecalcTotal()
{
decimal total = 0;
foreach (InvoiceDetail tmp in this.InvoiceDetails)
{
total += tmp.SubTotal;
}
//We need to update the Total property
//Total = total; ==> this is the normal way to do this ,
//but for debugging purposes I do it in the following way:
LoadProperty<decimal>(TotalProperty, total);
OnPropertyChanged("Total"); //DevExpress Putting this in cmment and the new item row from the DevExpress grid keeps it focus.
}
Steps to Reproduce:
In the example you can find at the top a DataGridView and at the bottom a devexpress grid.
When you execute the following steps with the devexpress grid:
- Create a new line by filling in "Product2" in the column ProductName
- Click Tab or -> (arrow right) to go to the next cell
- Fill in 1 in the column Quantity
- Click Tab or -> (arrow right) to go to the next cell
- Fill in 10 in the column UnitPrice
- when now clicking Tab or -> (arrow right) the row is been committed and loses his focus. The first row gets the focus.
Expected Results:
When you execute the following steps with the DataGrid from Microsoft: - Create a new line by filling in "Product2" in the column ProductName
- Click Tab or -> (arrow right) to go to the next cell
- Fill in 1 in the column Quantity
- Click Tab or -> (arrow right) to go to the next cell
- Fill in 10 in the columnUnitPrice
- when now clicking Tab or -> (arrow right) the cell SubTotal becomes the active cell.
Hello,
Your approach would work, if changing a property fired a single PropertyChanged notification. But OnChildChanged() / RecalcTotal() also raises a ListChanged event with the ListChangedType.Reset parameter. "Reset" stands for major changes made to the list, and the grid is completely refreshed. The XtraGrid's behavior is qualified as by design.
Thanks,
Nick
--------------------
Check if Search Engine is able to answer questions faster than I do!
When I implement the following code in the class InvoiceDetails we avoid raising a ListChanged event with ListChangedType.Reset.
But this doesn't solve problem. When you debug and put a breakpoint you'll see that even the ListChanged event is not called with ListChangedType.Reset when calling OnPropertyChanged("Total"). It is been called with ListChangedType.ItemChanged.
protected override void OnListChanged(System.ComponentModel.ListChangedEventArgs e)
{
if (e.ListChangedType == ListChangedType.Reset)
{
//do nothing
}
base.OnListChanged(e);
}
Hello,
The InvoiceDetails.OnListChanged method is not executed, just because the InvoiceDetails collection is not used as a data source for the grid.
However, if you attach a ListChanged event handler to the grid's DataSource, you can log ListChangedType.Reset notifications:
private void gridView1_DataSourceChanged(object sender, EventArgs e) { IBindingList ds = (IBindingList)gridView1.DataSource; ds.ListChanged += new ListChangedEventHandler(ds_ListChanged); } void ds_ListChanged(object sender, ListChangedEventArgs e) { if(e.ListChangedType == ListChangedType.Reset) Console.WriteLine("ListChanged Reset"); }
Here is the call stack:
> ApplicationLayer.exe!ApplicationLayer.Form1.ds_ListChanged(object sender = { [System.Windows.Forms.BindingSource]}, System.ComponentModel.ListChangedEventArgs e = {System.ComponentModel.ListChangedEventArgs}) Line 48 C#
[Native to Managed Transition]
[Managed to Native Transition]
System.Windows.Forms.dll!System.Windows.Forms.BindingSource.OnListChanged(System.ComponentModel.ListChangedEventArgs e) + 0x7e bytes
System.Windows.Forms.dll!System.Windows.Forms.BindingSource.ResetBindings(bool metadataChanged) + 0x59 bytes
System.Windows.Forms.dll!System.Windows.Forms.BindingSource.SetList(System.Collections.IList list, bool metaDataChanged, bool applySortAndFilter = true) + 0xf0 bytes
System.Windows.Forms.dll!System.Windows.Forms.BindingSource.ParentCurrencyManager_CurrentItemChanged(object sender, System.EventArgs e) + 0x12b bytes
[Native to Managed Transition]
[Managed to Native Transition]
System.Windows.Forms.dll!System.Windows.Forms.CurrencyManager.OnCurrentItemChanged(System.EventArgs e) + 0x17 bytes
System.Windows.Forms.dll!System.Windows.Forms.CurrencyManager.List_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e) + 0x3bc bytes
System.Windows.Forms.dll!System.Windows.Forms.BindingSource.OnListChanged(System.ComponentModel.ListChangedEventArgs e) + 0x7e bytes
System.Windows.Forms.dll!System.Windows.Forms.BindingSource.InnerList_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e) + 0x2e bytes
System.dll!System.ComponentModel.BindingList<System.__Canon>.OnListChanged(System.ComponentModel.ListChangedEventArgs e) + 0x17 bytes
System.dll!System.ComponentModel.BindingList<InvoiceLibrary.Invoice>.Child_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + 0x176 bytes
Csla.dll!Csla.Core.BindableBase.OnPropertyChanged(string propertyName = "Total") + 0x82 bytes
Invoice.Library.dll!InvoiceLibrary.Invoice.RecalcTotal() Line 43 + 0xe bytes C#
Thanks,
Nick
I see what you mean the gridview is not bound directly with the InvoiceDetails collection. The grid is bound to invoiceDetailsBindingSource with the following properties :
DataMember : InvoiceDetails
DataSource : invoiceBindingSource
So indirectly the grid is bound to the InvoiceDetails collection.
Makes this is a difference?
But the focus is not moved in the following cases (even when a ListChanged event is raised with ListChangedType.Reset)
- adding a new line and filling in the first column ProductName
- editing an existing line (column Quantity or column UnitPrice)
So in these cases the focus is not moved. So the behavior is not so consisted?
Hello,
In this situation the grid cannot restore the focused row after a List Reset notification. If you know when to save and restore the grid's focused row, you can try to work around this limitation. The focused row index can be set via the GridView.FocusedRowHandle property; the underlying data object can be retrieved via the GridView.GetRow method.
Thanks,
Nick
Restoring the focused row is not an option for me, because when the row loses it's focus it is also been committed and the user doesn't have anymore the capability to undo his changes with ESC. So I need to find another solution.
What I don't undertstand is who is triggering the ListChanged event with ListChangedType.Reset.
I've read on the Internet that OnPropertyChanged event can trigger ListChangedType.Reset event if OnPropertyChanged is been called with string.empty as parameter, but I'm calling it with a propertyName : OnPropertyChanged("Total");
Hi,
This may be a problem with CSLA .NET Framework, but I'm not sure. Please write to CSLA and ask them for assistance.
Thanks,
Nick
I've opened already a case on the forum of CSLA. (http://forums.lhotka.net/forums/thread/31603.aspx)
I've discovered in which case XtraGrid will work. This will maybe help you to debug it.
So in the example in the form you'll find two BindingSource controls.
invoiceBindingSource with the following properties
DataSource: InvoiceLibrary.Invoice
invoiceDetailsBindingSource with the following properties
DataSource: invoiceBindingSource
DataMember: InvoiceDetails
In this case the grid doesn't behave correctly. The problem is that when adding a new line and filling in Quantity and afterwards UnitPrice in the XtraGrid, the new item row loses its focus. So the user doesn't have anymore the capability to undo his changes.
When I put the following properties on invoiceDetailsBindingSource
DataSource: InvoiceLibrary.Invoice
DataMember: InvoiceDetails
the grid behaves correctly.
So when using one BindingSource as the data source for another BindingSource, the XtraGrid doesn't behave well.
Hi,
Tetranz is right; whenever a parent property is changed, a child list is refreshed. To work around this behavior, you can make the Total property in Invoice non-persistent, and calculate its value dynamically, on demand.
Thanks,
Nick
Yes, absolutely this is standard Windows Forms behavior! When a PropertyChanged event occurs on the root object it causes a refresh of all bindings for the root object. if the root object has child objects that are bound, those bindings are refreshed too.
Regarding a PropertyChanged resetting the list - what must be actually happening is that the PropertyChanged at the parent level tells data binding to refresh the controls. As part of that process it must tell child bindingsource objects to refresh as well, and that in turn must somehow be triggering the reset of the collection.
I can easily think of a scenario where this is required: order entry. Suppose the tax rate or discount rate are on the Order (parent) and you change it. All the items in the LineItems collection would need to recalculate and be redisplayed - and I'm sure data binding is trying to help you make sure this happens.
As explained in my last reply the XtraGrid is working well
when you put the following properties on invoiceDetailsBindingSource
DataSource: InvoiceLibrary.Invoice (==> not using the other bindingsource invoiceBindingSource)
DataMember: InvoiceDetails
Don't forget to put
invoiceDetailsBindingSource.DataSource = invoice.InvoiceDetails;
in the Form1_Load event because the datasource of invoiceDetailsBindingSource is not anymore feeded with the invoiceBindingSource.
So in this case also ListChanged event is triggered with Reset, but here it is working. So the grid is not consistent in behavior.
So apparently the XtraGrid doesn't work with nested bindingsources. I've tested the same example with FarPoint Spread and this control is working in both cases (nested and not nested bindingsources).
Please can you investigate this bug, because we are developping an application where we need this a lot.
Hello,
Okay, if the Reset event cannot be blocked in the data source, you can block it in the XtraGrid by creating a custom GridView. I've written the required classes and modified Form1. Please take the new modules from the attachment.
Please note that no changes have been made to the XtraGrid source code. The grid cannot distinguish the ListChanged Reset event that occurs in your situation from ListChanged Reset events in all other cases. Thus, the grid catches the ListChanged Reset notification and reloads rows. This behavior is by design and must not be changed.
Thanks,
Nick
I’ve tested your solution and this is working. Thanks for it. Thanks for the attachment.
I’ve looked also at the source code to find the reason why?
So when are modifying the column Quantity or UnitPrice of an InvoiceDetail, the property Total of the Invoice will be recalculated and changed automatically in the business layer, and this property Total will raise a PropertyChanged event so that the UI can adapt.
So I was wondering why there is a different behavior when adding a row or modifying a row in the grid.
While modifying a row, the grid has a behavior as I suspect.
While adding a row, the added row will be committed automatically and the first line in the grid gets the focus. So the user can’t undo his changes anymore and the newly added row loses his focus.
This is due to the fact of the following code in DevExpress.Data.Helpers
public override void OnRestoreEnd() {
base.OnRestoreEnd();
if((this.groupCount == 0 || this.foundCurrentRow == DataController.InvalidRow) && Controller.GroupedColumnCount > 0 && Controller.GroupRowCount > 0) {
if(Controller.CurrentControllerRow != CurrencyDataController.FilterRow)
Controller.CurrentControllerRow = Controller.GroupInfo[0].Handle;
} else {
if(this.foundCurrentRow == DataController.InvalidRow) this.foundCurrentRow = 0;
if(Controller.KeepFocusedRowOnUpdate)
Controller.CurrentControllerRow = this.foundCurrentRow;
}
}
So in the case we are adding a row the variable foundCurrentRow will have DataController.InvalidRow as value.
So while adding a row, this method sets the foundCurrentRow to 0 (the first row in the grid).
In the case of modifying a row the variable foundCurrentRow will not be set to 0.
So if you leave out this part of code
if(this.foundCurrentRow == DataController.InvalidRow) this.foundCurrentRow = 0;
the grid will behave correctly.
Can you look with the developper team?
In the solution you’ve send you are short circuiting the code and the method OnRestoreEnd will not be called.
This short circuit you can find in the method RaiseOnBindingListChanged in DevExpress.Data.CurrencyDataController
protected internal override void RaiseOnBindingListChanged(ListChangedEventArgs e) {
if(ValidationClient.BoundControl != null && ValidationClient.BoundControl.InvokeRequired) {
if(!_DisableThreadingProblemsDetection) {
ValidationClient.BoundControl.BeginInvoke(new MethodInvoker(ThrowCrossThreadException));
ThrowCrossThreadException();
}
ValidationClient.BoundControl.BeginInvoke(new ListChangedDelegate(OnBindingListChanged), new object[] { e });
System.Threading.Thread.Sleep(1);
return;
}
RaiseOnBeforeListChanged(e);
try {
if(e.ListChangedType == ListChangedType.Reset && IsCurrencyResetEventLocked && IsNewItemRowEditing) return;
OnBindingListChanged(e);
if(this.finishingNewItemRowEdit) return;
CurrentControllerRow = CurrentControllerRow;
}
finally {
RaiseOnListChanged(e);
}
}
In the solution you are given the variable IsCurrencyResetEventLocked will be set to true by calling LockCurrencyResetEvent, which will be called by the following code
private void myGridView1_ShownEditor(object sender, EventArgs e)
{
MyGridView view = (MyGridView)sender;
((MyViewDataController)view.DataController).LockResetEvent();
}
So would it be possible to look at the solution I describe with the developper team. Because in my opion the grid should have the same behavior in add or modify modus, because in both cases you’ll have ListChangedType.Reset.
Hello,
It's very risky to change that code. All those flags and checks were added to support the XtraGrid's functionality in various binding scenarios and situations. The developer who wrote that code is currently on vacation. Your issue report will remain open for two weeks, until the developer comes back and analyzes the problem. Sorry, but this time we can't promise that your proposal will be included in our code. You may wish to modify the source code of our components on your system, rebuild the assemblies, and test them. You can also use the solution with inherited classes, suggested above.
Thanks,
Nick
Hello,
It's been decided not to change the OnRestoreEnd method.
Thanks,
Nick