Microsoft KB Archive/317088

= BUG: Memory Leak in Windows Forms Application When You Move Through Master-Detail DataGrid Controls =

PSS ID Number: 317088

Article Last Modified on 3/28/2002

-

The information in this article applies to:


 * Microsoft Visual Studio .NET (2002), Professional Edition

-



This article was previously published under Q317088



SYMPTOMS
If you use two DataGrid controls in a Windows Forms application to display parent and child records from two related DataTable objects in a DataSet object (a master-detail form), when you move through the DataGrid controls, objects are created that the common language runtime never garbage collects. This may lead to a memory leak, which affects performance. Additionally, this memory leak increases every time the user or the code moves from one record to another.



CAUSE
The memory leak occurs because the DataTable objects continue to hold references to objects that they no longer use after the current record changes. As a result, those objects cannot be garbage collected. This affects performance because DataTable events must be propagated to the growing list of unused objects that causes the memory leak.



RESOLUTION
Because the relation that is established between the two DataTable objects is a condition of this problem, you can delete the relation and manually update the child DataGrid to synchronize its display with the record that is selected in the parent DataGrid. For more information about how to implement this workaround, see the &quot;More Information&quot; section.



STATUS
Microsoft has confirmed that this is a bug in the Microsoft products that are listed at the beginning of this article.



Steps to Reproduce Behavior
The following sample uses Microsoft SQL Server and the Northwind sample database. You can also reproduce this problem by using the OLE DB managed provider and any two database tables that have a parent-child relationship.

Add the Controls

 * 1) Start Microsoft Visual Studio .NET.
 * 2) On the File menu, point to New, and then click Project.
 * 3) In the New Project dialog box, click Visual Basic Projects under Project Types, and then click Windows Application under Templates.
 * 4) Drag three DataGrid controls from the toolbox onto the form.
 * 5) Drag two Button controls from the toolbox onto the form.
 * 6) Change the Text property of the first button to Run Test, and then change the Text property of the second button to Run GC.

Add the Data Objects
 In Server Explorer, configure a new data connection to the Northwind sample database on an available SQL Server. Expand the new data connection and its list of tables. Drag the Customers and the Orders tables to the form. Notice that SQLConnection1, SQLDataAdapter1, and SQLDataAdapter2 appear in the component tray under the form. Right-click SQLDataAdapter1, and then click Generate Dataset. In the Generate Dataset dialog box, change the name of the new DataSet object to CustomerOrders. Make sure that the following check boxes are selected: <ul> Customers</li> Orders</li> Add this dataset to the designer</li></ul>

</li> Click OK. Notice that CustomerOrders1 is added to the component tray.</li> Right-click CustomerOrders1, and then click View Schema.</li> In the Schema Designer dialog box, drag a Relation object from the toolbox to the CustomerID text box in the Orders table. (CustomerID is the foreign key that relates the Orders table to the Customers table.) Notice that the Edit Relation dialog box opens. Click OK to accept the default settings.</li></ol>

Add Code to the Windows Application
<ol> Open the form's code window.</li>  Add the following Imports statements to the top of the code window: Imports System.Data Imports System.Data.SqlClient Imports System.ComponentModel </li>  Under the Public Class Form1 statement and the Inherits statement, add the following module-level variable declarations to the top of the class: Dim ds As DataSet Dim dt As DataTable Dim dr As DataRow Dim bc As BindingContext </li>  In the Form_Load event, add the following code to retrieve the data and to bind the DataGrid controls: SqlDataAdapter1.Fill(CustomerOrders1, &quot;Customers&quot;) SqlDataAdapter2.Fill(CustomerOrders1, &quot;Orders&quot;) DataGrid1.DataSource = Me.CustomerOrders1 DataGrid1.DataMember = &quot;Customers&quot; DataGrid2.DataSource = Me.CustomerOrders1 DataGrid2.DataMember = &quot;Customers.CustomersOrders&quot; NOTE: You can double-click the form to generate the Sub...End Sub statement. </li>  In the Button1_Click event, add the following code: Dim iter As Integer, pos As Integer Dim mem As Integer, diff As Integer, cumul As Integer CreateResultsDatatable 'Obtain initial values before you start. dr = dt.NewRow dr(&quot;Iteration&quot;) = 0 dr(&quot;Memory&quot;) = GC.GetTotalMemory(False) dr(&quot;Increase&quot;) = 0 dr(&quot;Cumulative&quot;) = 0 dt.Rows.Add(dr) 'Loop through the DataTable rows several times. bc = Me.BindingContext For iter = 1 To 6 bc(Me.CustomerOrders1, &quot;Customers&quot;).Position = 0 For pos = 0 To bc(Me.CustomerOrders1, &quot;Customers&quot;).Count - 1 bc(Me.CustomerOrders1, &quot;Customers&quot;).Position = pos Next pos Application.DoEvents mem = GC.GetTotalMemory(False) diff = mem - dt.Rows(iter - 1)(1) cumul = mem - dt.Rows(0)(1) 'Save values for this iteration. dr = dt.NewRow dr(&quot;Iteration&quot;) = iter dr(&quot;Memory&quot;) = mem dr(&quot;Increase&quot;) = diff dr(&quot;Cumulative&quot;) = cumul dt.Rows.Add(dr) Next iter DataGrid3.DataSource = dt       FormatResultsGrid </li>  Before the End Class statement, add the following supporting subroutines to create an additional DataTable for test results and to format the DataGrid in which the table is displayed: Private Sub CreateResultsDatatable Dim dttemp As DataTable ds = New DataSet dttemp = New DataTable(&quot;Results&quot;) With dttemp .Columns.Add(&quot;Iteration&quot;, GetType(Integer)) .Columns.Add(&quot;Memory&quot;, GetType(Integer)) .Columns.Add(&quot;Increase&quot;, GetType(Integer)) .Columns.Add(&quot;Cumulative&quot;, GetType(Integer)) End With ds.Tables.Add(dttemp) dt = ds.Tables(&quot;Results&quot;) End Sub

Private Sub FormatResultsGrid Dim ts As New DataGridTableStyle Dim pcol As PropertyDescriptorCollection pcol = bc(ds, &quot;Results&quot;).GetItemProperties ts.MappingName = &quot;Results&quot; Dim col1 As New DataGridTextBoxColumn With col1 .MappingName = &quot;Iteration&quot; .HeaderText = &quot;Iteration&quot; .Width = 50 .Alignment = HorizontalAlignment.Center End With ts.GridColumnStyles.Add(col1) Dim col2 As New DataGridTextBoxColumn(pcol(&quot;Memory&quot;), &quot;#,##0&quot;) With col2 .MappingName = &quot;Memory&quot; .HeaderText = &quot;Memory&quot; .Width = 75 .Alignment = HorizontalAlignment.Right End With ts.GridColumnStyles.Add(col2) Dim col3 As New DataGridTextBoxColumn(pcol(&quot;Increase&quot;), &quot;#,##0&quot;) With col3 .MappingName = &quot;Increase&quot; .HeaderText = &quot;Increase&quot; .Width = 75 .Alignment = HorizontalAlignment.Right End With ts.GridColumnStyles.Add(col3) Dim col4 As New DataGridTextBoxColumn(pcol(&quot;Cumulative&quot;), &quot;#,##0&quot;) With col4 .MappingName = &quot;Cumulative&quot; .HeaderText = &quot;Cumulative&quot; .Width = 75 .Alignment = HorizontalAlignment.Right End With ts.GridColumnStyles.Add(col4) DataGrid3.TableStyles.Add(ts) End Sub </li>  In the Button2_Click event, add the following code: 'Force garbage collection. GC.Collect GC.WaitForPendingFinalizers 'Add final values to DataGrid. dr = dt.NewRow dr(1) = GC.GetTotalMemory(False) dr(3) = dr(1) - dt.Rows(0)(1) dt.Rows.Add(dr) </li></ol>

Test the Project
<ol> Run the project. Notice that DataGrid1 displays a list of customers, and that DataGrid2 displays a list of orders for the current customer.</li> Click Run Test. This test loops several times through each record in the Customers table to reproduce the memory leak. Notice that DataGrid3 displays the results.</li> Examine the test results in DataGrid3.

<ul> <li>The Memory column displays the memory that the managed heap uses, which is subject to garbage collection.</li> <li>The Increase column displays the additional heap memory that each individual iteration uses.</li> <li>The Cumulative column displays the heap memory that the whole test uses.</li> <li>The test results indicate that the program leaks about 4 kilobytes (KB) every time the current row changes (that is, whenever the user or the code moves from one row to another) in the parent table.</li></ul>

NOTE: Memory values are displayed in bytes. Iteration 0 represents the values before navigation begins.</li> <li>Click Run GC two or three times to force garbage collection.</li> <li>Examine the results that are added to DataGrid3. Although garbage collection slightly reduces the total memory that is used the first time that garbage collection runs, the values remain fairly consistent thereafter. This confirms that the memory leak occurs because objects are not garbage collected.</li></ol>

Workaround
To work around this problem, modify the project as follows: <ol> <li>Open the DataSet schema, and then delete the relation between the two DataTable objects.</li> <li> Add the following declarations at the top of the form's class code: Dim dv As DataView Dim cm as CurrencyManager </li> <li> Replace the following code in the Form_Load event DataGrid2.DataSource = Me.CustomerOrders1 DataGrid2.DataMember = &quot;Customers.CustomersOrders&quot; with the following code: dv = New DataView(CustomerOrders2.Orders) DataGrid2.DataSource = dv </li> <li> At the end of the Form_Load event code, add the following code to configure an event handler for the PositionChanged event: cm = Me.BindingContext(CustomerOrders2, &quot;Customers&quot;) AddHandler cm.PositionChanged, AddressOf DataGridRowChanged Each time the current record changes in the parent Customers table, you use the PositionChanged event to synchronize the display in the Orders grid. </li> <li> Add the event procedure for the PositionChanged event: Protected Sub DataGridRowChanged(ByVal sender As Object, _           ByVal e As EventArgs) dv.RowFilter = &quot;CustomerID='&quot; & _ CustomerOrders2.Customers(cm.Position)(&quot;CustomerID&quot;)& &quot;'&quot; End Sub </li> <li>Run the project again, and then examine the test results in DataGrid3. Notice that the memory leak no longer occurs.</li></ol>

Additional query words: navigate

Keywords: kbbug kbDatabase kbDataBinding kbDSupport KB317088

Technology: kbAudDeveloper kbVSNETPro kbVSNETSearch kbVSsearch

-

[mailto:TECHNET@MICROSOFT.COM Send feedback to Microsoft]

© 2004 Microsoft Corporation. All rights reserved.