DatagridView Scroll Jump When Editing Bound Cells

Posts   
 
    
Steve
User
Posts: 24
Joined: 20-Dec-2004
# Posted on: 14-Nov-2011 23:11:45   

WinForms VS2010 / .NET 4.0 LLBLGen 2.6.8.1114 SQL 2008 R2

Problem: When binding an editable datagridview to a sorted entitycollection (or entityview), the cursor jumps unexpectedly when completing edit of one cell and moving to another row.

Reproduction steps:

Scenario One: Sort the grid by a column other than the first. Navigate to a cell in the sorted column (identified by the sort caret). Change its value. Use down arrow to navigate to the cell below or above. The cursor jumps to the beginning of the row of the cell that was selected by the arrow key.

Scenario Two (requires enoungh data for the vertical scrollbar to be present): Scroll down the grid at least one page to one of the interior rows. Navigate to a cell in the non-sorted column (not the first column) for a row somewhere in the middle of the viewable rowset. Change its value. Use down arrow to navigate to the cell below or above. The cursor jumps to the beginning of the row of the cell that was selected by the arrow key AND the scroll position jumps so that the active row is at the bottom of the viewable rows.

Any help would be appreciated. Thanks!

namespace TestA { partial class frmGridTest { /// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing) {
        if (disposing && (components != null)) {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent() {
        this.dataGridView1 = new System.Windows.Forms.DataGridView();
        ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
        this.SuspendLayout();
        // 
        // dataGridView1
        // 
        this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
        | System.Windows.Forms.AnchorStyles.Left) 
        | System.Windows.Forms.AnchorStyles.Right)));
        this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
        this.dataGridView1.Location = new System.Drawing.Point(12, 12);
        this.dataGridView1.Name = "dataGridView1";
        this.dataGridView1.Size = new System.Drawing.Size(580, 311);
        this.dataGridView1.TabIndex = 0;
        // 
        // frmGridTest
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(604, 335);
        this.Controls.Add(this.dataGridView1);
        this.Name = "frmGridTest";
        this.Text = "frmGridTest";
        this.Load += new System.EventHandler(this.frmGridTest_Load);
        ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
        this.ResumeLayout(false);

    }

    #endregion

    private System.Windows.Forms.DataGridView dataGridView1;
}

}

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using BLXXX.EntityClasses; using SD.LLBLGen.Pro.ORMSupportClasses; using System.Data.SqlClient;

namespace TestA { public partial class frmGridTest : Form {

    public frmGridTest() {
        InitializeComponent();
    }

    private void frmGridTest_Load(object sender, EventArgs e) {
        //Any Entity with a Child Collection
        HeaderEntity entHeader = new HeaderEntity(1);
        //Get the child collection as a Default View
        SD.LLBLGen.Pro.ORMSupportClasses.EntityView<DetailEntity> evDetail = entHeader.Detail.DefaultView;
        //Set any sort
        evDetail.Sorter = new SortExpression(new SortClause(BLXXX.HelperClasses.DetailFields.ItemCode, SortOperator.Ascending));
        //Bind the DataGridView to the datagridview
        dataGridView1.DataSource = evDetail;
    }
}

}

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 15-Nov-2011 07:07:01   

Hi Steve,

Unless I'm wrong, I don't see how this is related to LLBLGen. I rather would inspect the grid's events and properties to see what is its behavior.

David Elizondo | LLBLGen Support Team
Steve
User
Posts: 24
Joined: 20-Dec-2004
# Posted on: 15-Nov-2011 16:56:22   

Sorry - I did leave out some information. This problem does not occur when binding the same grid to a sorted DataView.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 15-Nov-2011 20:17:18   

Just for testing purpose please try the following:

EntityView<DetailEntity> evDetail = entHeader.Detail.DefaultView;

//Set any sort
evDetail.Sorter = new SortExpression(new  
     SortClause(BLXXX.HelperClasses.DetailFields.ItemCode, SortOperator.Ascending));

// try this
evDetail.DataChangeAction = PostCollectionChangeAction.NoAction;

//Bind the DataGridView to the datagridview
dataGridView1.DataSource = evDetail;
David Elizondo | LLBLGen Support Team
Steve
User
Posts: 24
Joined: 20-Dec-2004
# Posted on: 15-Nov-2011 20:56:10   

OK, some good news - that does eliminate the cursor/scroll jump. But (as you know) that also stops column resorting. Is there a middle ground?

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 15-Nov-2011 21:16:06   

Steve wrote:

OK, some good news - that does eliminate the cursor/scroll jump. But (as you know) that also stops column resorting. Is there a middle ground?

The problem is: when you change some entity values, the view is resorted, that sends a signal to the boundable object (the grid) which re-paints itself. That, I think it is unavoidable and that is the cause of the "cursor jump" that you see. On the other hand, if you set the EntityView to NoAction, the collection is sorted first, then when you change something the sort is no re-applied so the grid wont repaint itself.

A workaround, or more like a hack could be that you catch the grid's event when you change the current cell and grab it somewhere. Then you catch the event fired when the sorter is re-applied, (maybe this?) Then you force the current cell to the cell you grabbed.

Or, just live with it. Set the postAction to NoAction and place a button to re-apply the sorter, or when a user clicks a column to be sorted. I don't know what else to advice.

David Elizondo | LLBLGen Support Team
Steve
User
Posts: 24
Joined: 20-Dec-2004
# Posted on: 15-Nov-2011 22:30:52   

Dave, thanks for info.

I have to disagree on the 'unavoidable' aspect. If you look at how the grid behaves when it's bound to a .NET DataView, it does not reset its view, and it does continue to reapply sorting.

How does LLBLGen handle EntityView resorting? Behind the scenes, is a 'new' collection (composed of the original entities) created, with entities added in the requested order? If this were the case, it could explain why the grid resets when it's bound to an EntityView and the sort is reapplied, because the grid thinks it's essentially being handed a new collection.

Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 16-Nov-2011 09:16:35   

The grid binds to an EntityView, even if you bind it manually to an EntityCollection, it gets the Default EntityView. And in the case of re-applying the sorter or the filter, it gets a new EntityView.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 16-Nov-2011 11:05:00   

Steve wrote:

Dave, thanks for info. I have to disagree on the 'unavoidable' aspect. If you look at how the grid behaves when it's bound to a .NET DataView, it does not reset its view, and it does continue to reapply sorting.

I've to dig with reflector into the DataView class, but I'm pretty sure they also issue a IBindingList.ListChanged event sending a Reset (like we do too). The only thing which might be different is that they likely don't re-sort when you change a value, or only after you move away from the complete row. We do that per field.

How does LLBLGen handle EntityView resorting? Behind the scenes, is a 'new' collection (composed of the original entities) created, with entities added in the requested order? If this were the case, it could explain why the grid resets when it's bound to an EntityView and the sort is reapplied, because the grid thinks it's essentially being handed a new collection.

It maintains an index list of the entities in the sorted order. The enumerator then uses the index list to index into the real collection to return the entities in the sorted order (and also filtered, so only entities which are in the view have their index in the index list). This allows multiple views on the same collection with different filters/sorters.

When an entity changes, the order can be different. So it re-applies the sorter/filter. It then simply raises IBindingList.ListChanged with the 'Reset' parameter, so the bound control has to rebind the data. Otherwise it would have to raise a lot of 'ItemMoved' ListChanged events, after everything was sorted.

The main problem is: how does the bound control identify rows bound to it? most controls have an extra code path for datatables/views and likely peek into the pkfields set of the bound datatable to identify rows (so a reset won't select a different row). In case of bound objects/entities it might not do that and simply select the top row for the active row/selected row. But that's out of our hands.

In the designer we had the same problem with the devexpress grids and a BindingList<some object> so not our entity views: when a value changed, the rows would get re-sorted, but we wanted to keep the same active row visible. We had to re-state what the active row was after a change.

Sorting has to take place at some point when data changes, this is unavoidable (I hope you agree with that wink ) The main issue then is: when to do this? And after sorting takes place, what is the desired result? I think that last question is the main one: what do you want after changing a cell to happen with the row you're editing? Should that still be the active row or not? The row you edited could be moved (due to the sorting) to a place in the list far below or far above the list of rows currently viewed by the grid. Do you want to keep viewing those rows, or do you want to move up/down to the row you were editing?

I don't know in what kind of behavior a datatable bound to a datagridview results in with these questions, but it seems to me you have either tell the datagridview how to identify the rows or do the manual selection after sorting/changing.

Frans Bouma | Lead developer LLBLGen Pro
Steve
User
Posts: 24
Joined: 20-Dec-2004
# Posted on: 16-Nov-2011 16:42:25   

Otis wrote:

I've to dig with reflector into the DataView class, but I'm pretty sure they also issue a IBindingList.ListChanged event sending a Reset (like we do too).

I think this is the difference. I don't believe the DataView issues a ListChanged event (with reset), based on the the behavioral difference. If it did, I would expect the view to reset when moving between rows (not just fields) following edits, but it doesn't. The individual row that was just edited may be shuffled to a new location (which is expected), but the entire view does not reset. In the case of binding an editable grid to an EntityView, the reset significantly reduces usability. disappointed

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 17-Nov-2011 09:59:12   

Steve wrote:

Otis wrote:

I've to dig with reflector into the DataView class, but I'm pretty sure they also issue a IBindingList.ListChanged event sending a Reset (like we do too).

I think this is the difference. I don't believe the DataView issues a ListChanged event (with reset), based on the the behavioral difference. If it did, I would expect the view to reset when moving between rows (not just fields) following edits, but it doesn't. The individual row that was just edited may be shuffled to a new location (which is expected), but the entire view does not reset. In the case of binding an editable grid to an EntityView, the reset significantly reduces usability. disappointed

As I don't have the sourcecode nor the design documents of the datatable + dataview I can't answer it for certain. What I can see with reflector is that it uses refresh in some occasion, not reset, but when that's issued is unclear. Also, as they issue a refresh as far as I can see, the sorter is never re-applied. I can't find a call to a method which reapplies a sorter when a column field in the bound table changes. This might be why you don't see the behavior there, as they don't re-apply sorting. Modern grids also do sorting outside the view btw, they sort the data in the grid itself, not order the data to sort (as IBindingList based sorting is limited to 1 column). The datagridview doesn't do that however.

As I described above, it's not the end of the world, you have to do the behavioral change of the grid yourself, by switching off sorter application during edit. This isn't that odd, considering it's unclear what you might want (as more possibilities are possible as I described).

We have scheduled an update for the event raising, but when that will be implemented is not planned yet.

Frans Bouma | Lead developer LLBLGen Pro
Steve
User
Posts: 24
Joined: 20-Dec-2004
# Posted on: 17-Nov-2011 22:01:47   

As I described above, it's not the end of the world, you have to do the behavioral change of the grid yourself, by switching off sorter application during edit.

You're absolutely right. The best approach is just to set the PostCollectionChangeAction to NoAction. Thank you, everyone!