dotnet / winforms

Windows Forms is a .NET UI framework for building Windows desktop applications.
MIT License
4.41k stars 984 forks source link

Serialization causes DataGridViewColumns to be defined after they are added to DataGridView #8697

Closed paul1956 closed 2 weeks ago

paul1956 commented 1 year ago

Environment

Microsoft Visual Studio Community 2022 (64-bit) - Preview Version 17.6.0 Preview 1.0

.NET version

N/A

Did this work in a previous version of Visual Studio and/or previous .NET release?

the same bug is present in VS2019

Issue description

        ' DgvCareLinkUsers
        ' 
       ' Stuff removed
        Me.DgvCareLinkUsers.Columns.AddRange(New DataGridViewColumn() {Me.DgvCareLinkUsersUserID, Me.DgvCareLinkUsersDeleteRow, Me.DgvCareLinkUsersCareLinkUserName, Me.DgvCareLinkUsersCareLinkPassword, Me.DgvCareLinkUsersCountryCode, Me.DgvCareLinkUsersUseLocalTimeZone, Me.DgvCareLinkUsersAutoLogin})
        Me.DgvCareLinkUsers.DataSource = Me.CareLinkUserDataRecordBindingSource
        ' Stuff Removed
        ' 
        ' DgvCareLinkUsersUserID
        ' 
        Me.DgvCareLinkUsersUserID.DataPropertyName = "ID"
        Me.DgvCareLinkUsersUserID.HeaderText = "ID"
        Me.DgvCareLinkUsersUserID.Name = "DgvCareLinkUsersUserID"
        Me.DgvCareLinkUsersUserID.ReadOnly = True
        Me.DgvCareLinkUsersUserID.Width = 43

If DgvCareLinkUsers has a Handler on ColumnAdded can't use DataPropertyName or Name because they are initialized AFTER the column is added. Those values are blank at the time of the add.

Steps to reproduce

Add DataGridView and then some columns with DataPropertyName and Name set

The AddRange statement happens BEFORE the values of the columsn are filled in

Diagnostics

N/A
paul1956 commented 1 year ago

This is a bigger issue for Visual Basic because it allows Event Handlers to be setup BEFORE Form is initialized so the order of control initialization matters.

elachlan commented 1 year ago

@Olina-Zhang could your team please confirm this bug and create an issue in the designer repo?

Olina-Zhang commented 1 year ago

@paul1956 We tested your mentioned scenarios as we designed, .Net framework designer and previous OOP designer have same behavior, could you please look at following to see if it is consitent with yours? If so, @elachlan I am not sure if it is a real issue.

Steps:

  1. Create a Winforms VB .Net Core application with DataGridView added
  2. Bind a DataSource to DataGridView and bind a Table with columns, in here we can see Columns with DataPropertyName or Name set
  3. Generate DataGridView's ColumnAdded event, and add some code with breakpoint
  4. Build and F5 app

Result: Form is not displayed and hitting that breakpoint in DataGridView's ColumnAdded event Record: 123 gif

paul1956 commented 1 year ago

@Olina-Zhang When you hit breakpoint, I can't tell if any column properties are set. I don't know how to stop video. Is you test app on GitHub?

I can repo this every time if I move adding columns of DGV to after creation of columns application works as expected. If I open Form in designer make any change to form (even just adding space to Title), the initialization of DataGridView is always moved above creation of columns and application breaks. In other cases, adding items to container happens after they have initialized Me.Add to populate controls on Form is always last. The same should be true for DataGridView. It's not that the columns aren't added, it's that they not initialized before the event is called so there is for example no way to know which columns are added except by column index which is a very bad way to design code.

In your example you are not looking at the columns in the ColumnsAdded handler you are just changing values that will be over written when the columns are initialized. In my example I look at the column HeaderText or other properties and take action based on them or modify them but because of initialization order all the fields are Nothing (Null). I hide certain columns based on their name for example.

Olina-Zhang commented 1 year ago

@paul1956 Could you please provide a sample application in order to understand your issue? Thanks a lot!

paul1956 commented 1 year ago

@Olina-Zhang

https://github.com/paul1956/DataGridViewSerializationFailure

All you need be concerned with is Form1 and the placement of the order of the column initialization with respect to the DataGridViews (I cloned the columns for simplicity). There is an #IF around failing code, and an identical handler in the other case.'

The one difference is the placement of the Initialization of the DataGridView. It only fails where there is a BindingSource.

After proving the Program runs as expected you have 2 choices, Open Form1 in designer make any change (add space to title) you will notice in Git there was a significant change to Form1.Designer, and save and run again and it will crash. Or #Define Failure and the other branch will fail with is identical to the other branch the difference is the location of the initialization in Form1.Designer.

Olina-Zhang commented 1 year ago

@paul1956 Thanks for your detail description. And we verified previous VS and the latest VS, all have same Initialization sequence for DataGridView in C# and VB, first is DataGridView Initialization, then column initialization as following screenshot. Once Form UI has some change, like add space to Form title you did, all of Designer initialization will be generated automatically, so you see the behavior about the DataGridView Initialization is moved before column initialization. It seems not a real issue. Please let our engineering team to confirm. @dreddy-work or @Tanya-Solyanik image

merriemcgaw commented 1 year ago

@KlausLoeffelmann can you take a look while you're working in Serialization?

paul1956 commented 1 year ago

@drago-draganov @Tanya-Solyanik @KlausLoeffelmann It is definitely a real issue when you have a BindingSource, the ColumnAdded Events happen before the columns are initialized. I have not seen, the code that deals with ColumnsAdded is very old.

It seems to me that all components should be initialized BEFORE they are added to a container like is done with Form (and some others) so that events work correctly. I don't see how that could break anything.

Tanya-Solyanik commented 1 year ago

@paul1956 - are you saying that BeginInit on DataGridView is not working anymore? Do you experience uniitialized values at the runtime -

If DgvCareLinkUsers has a Handler on ColumnAdded can't use DataPropertyName or Name because they are initialized AFTER the column is added. Those values are blank at the time of the add.

paul1956 commented 1 year ago

@Tanya-Solyanik I am not sure what BeginIntit is (don't know what it does) but yes, all the values that are set in Form1.Designer for the Columns are uninitialized when the handler gets called UNLESS I move the initialization of the DataGridView control (that contains the columns) until after the columns are initialized (which are the 2 cased shown in the code I posted) in Form1.Designer. The difference with VB vs. C# is that I can set up the handlers in Form1 BEFORE the Form is initialized so when the columns are added the Columns_Added handler is getting invoked but the columns are uninitialized.

UPDATE! I found this line in Form1.Designer which I assume is what you are taking about but don't know its purpose.

        CType(DgvCareLinkUsers, ComponentModel.ISupportInitialize).BeginInit()

If I remove the

    <System.Diagnostics.DebuggerStepThrough()>

If step through the code as soon as the columns are added the handler

    Private Sub DgvCareLinkUsers_ColumnAdded(sender As Object, e As DataGridViewColumnEventArgs) Handles DgvCareLinkUsers.ColumnAdded

is called and then I hit the column initializations.

KlausLoeffelmann commented 1 year ago

@paul1956, do you have the chance to create a small repro for this issue with VS 2019 .Net 5 by any chance, then open it with 2022 >=17.5, make a small size change to the form, save it, and show the delta of both InitializeComponent versions of 2019 and 2022? That would be extremely helpful! Could you also check, if in 2019 the behavior at runtime is still as expected?

paul1956 commented 1 year ago

I don't have 2019 but every version I had tried of 2022 moves the creation of the DataGridView before the columns are initialized which I thought was the issue but reading some of this thread it's not the order of initialization in DataGridView vs DataGridViewColumn but that beginInit is supposed to somehow delay the events from being called until the end of the initialization and that is what is not happening. I could try using Framework 4.X if that helps.

paul1956 commented 1 year ago

@KlausLoeffelmann I can't even download VS 2019.

KlausLoeffelmann commented 1 year ago

Does this work for you? https://visualstudio.microsoft.com/vs/older-downloads/

paul1956 commented 1 year ago

@KlausLoeffelmann thanks I downloaded it.

After changing name of Form1, the file is very different, but the same reordering happens, and the app will crash unless I move below from near top to bottom. Since BeginInit is designed to address this the issue is probably in there for DataGridView. Below fails;

        CType(Me.DgvCareLinkUsers, System.ComponentModel.ISupportInitialize).BeginInit()
        CType(Me.CareLinkUserDataListBindingSource, System.ComponentModel.ISupportInitialize).BeginInit()
        Me.SuspendLayout()
        '
        'DgvCareLinkUsers
        '
        Me.DgvCareLinkUsers.AllowUserToAddRows = False
        Me.DgvCareLinkUsers.AllowUserToResizeColumns = False
        Me.DgvCareLinkUsers.AllowUserToResizeRows = False
        Me.DgvCareLinkUsers.AutoGenerateColumns = False
        Me.DgvCareLinkUsers.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells
        Me.DgvCareLinkUsers.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCells
        Me.DgvCareLinkUsers.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize
        Me.DgvCareLinkUsers.Columns.AddRange(New System.Windows.Forms.DataGridViewColumn() {Me.AutoLoginDataGridViewCheckBoxColumn, Me.CareLinkPasswordDataGridViewTextBoxColumn, Me.CareLinkUserNameDataGridViewTextBoxColumn, Me.CountryCodeDataGridViewTextBoxColumn, Me.IDDataGridViewTextBoxColumn, Me.UseLocalTimeZoneDataGridViewCheckBoxColumn})
        Me.DgvCareLinkUsers.DataSource = Me.CareLinkUserDataListBindingSource
        Me.DgvCareLinkUsers.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter
        Me.DgvCareLinkUsers.Location = New System.Drawing.Point(0, 0)
        Me.DgvCareLinkUsers.Name = "DgvCareLinkUsers"
        Me.DgvCareLinkUsers.RowTemplate.Height = 25
        Me.DgvCareLinkUsers.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect
        Me.DgvCareLinkUsers.Size = New System.Drawing.Size(800, 450)
        Me.DgvCareLinkUsers.TabIndex = 0

Below works

        '
        'DgvCareLinkUsers
        '
        Me.DgvCareLinkUsers.AllowUserToAddRows = False
        Me.DgvCareLinkUsers.AllowUserToResizeColumns = False
        Me.DgvCareLinkUsers.AllowUserToResizeRows = False
        Me.DgvCareLinkUsers.AutoGenerateColumns = False
        Me.DgvCareLinkUsers.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells
        Me.DgvCareLinkUsers.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCells
        Me.DgvCareLinkUsers.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize
        Me.DgvCareLinkUsers.Columns.AddRange(New System.Windows.Forms.DataGridViewColumn() {Me.AutoLoginDataGridViewCheckBoxColumn, Me.CareLinkPasswordDataGridViewTextBoxColumn, Me.CareLinkUserNameDataGridViewTextBoxColumn, Me.CountryCodeDataGridViewTextBoxColumn, Me.IDDataGridViewTextBoxColumn, Me.UseLocalTimeZoneDataGridViewCheckBoxColumn})
        Me.DgvCareLinkUsers.DataSource = Me.CareLinkUserDataListBindingSource
        Me.DgvCareLinkUsers.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter
        Me.DgvCareLinkUsers.Location = New System.Drawing.Point(0, 0)
        Me.DgvCareLinkUsers.Name = "DgvCareLinkUsers"
        Me.DgvCareLinkUsers.RowTemplate.Height = 25
        Me.DgvCareLinkUsers.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect
        Me.DgvCareLinkUsers.Size = New System.Drawing.Size(800, 450)
        Me.DgvCareLinkUsers.TabIndex = 0
        '
        'Form1
        '
Tanya-Solyanik commented 1 year ago

@paul1956 - Do I understand correctly that this is not a regression from VS 2019?

paul1956 commented 1 year ago

I believe that is correct, I don't know the last time it worked. But given what BeginInit is supposed to do it's not doing it now.

This code predates .NetCore but only recent releases of 2022 Designer was functional for VB, so I just made adjustments by editing FormX.Design.VB and initialize of DataGridView was after Columns and they didn't move. Since the designer has gotten better I want to use it and the issue is obvious because every edit reorders the Designer file and breaks the application.

paul1956 commented 1 year ago

@KlausLoeffelmann If DataGridViewColumn initialization were turned into below it would simplify the code and address the BeginInit issue.

Me.DgvCareLinkUsersUserID = New DataGridViewTextBoxColumn With {
            .DataPropertyName = "ID",
            .HeaderText = "ID",
            .Name = "DgvCareLinkUsersUserID",
            .ReadOnly = True,
            .Width = 43
        }

Its similar to what is done for TableLayoutPanel Styles. without all the formatting

merriemcgaw commented 3 weeks ago

@Olina-Zhang can your team create this issue in the designer repo and we will try to get it on the backlog there. You can close this one.

Olina-Zhang commented 2 weeks ago

Created in designer repo: https://github.com/microsoft/winforms-designer/issues/6139, closing this one now.