Performance in Windows Forms GridGrouping (GridGroupingControl)

28 Apr 2021 / 24 minutes to read

GridGroupingControl has an extremely high performance standard. It can handle very high frequency updates and refresh scenarios. It also offers complete support for Virtual Mode wherein data will be loaded only on demand. Using some of the optimization properties, user can have the grid work with large amounts of data without a performance hit.

All the properties that affects grid performance are wrapped into a category named Optimization.

Performance_img1

Optimization Properties

In this section you will learn a brief explanation of the optimization properties available in GridGroupingControl. In the upcoming topics you will learn the usage of these properties in application.

Enabling Required Optimizations

AllowedOptimizations specifies the optimizations when the engine is allowed to use when applicable. These optimizations can be used in combination with CounterLogic setting.EngineOptimizations enum defines values for this property.

// Enables the DisableCounters, VirtualMode and RecordsAsDisplayElements optimizations.
this.gridGroupingControl1.AllowedOptimizations = Syncfusion.Grouping.EngineOptimizations.All;
' Enables the DisableCounters, VirtualMode and RecordsAsDisplayElements optimizations.
Me.gridGroupingControl1.AllowedOptimizations = Syncfusion.Grouping.EngineOptimizations.All

Reduce Flickering

AllowOptimizeLoadTime property helps in reducing the flickering issue at startup. When enabled, the grid will be rendered once into an offline bitmap before the form is shown for the first time. This offline rendering of the grid ensures that all the required data is loaded into the memory and all grid data are initialized. Default value is true.

// Reduces flickering at the start time
this.gridGroupingControl1.AllowOptimizeLoadTime = true;
' Reduces flickering at the start time
Me.gridGroupingControl1.AllowOptimizeLoadTime = True

Bypassing CurrencyManager

When the data table is assigned to the GridGroupingControl, it will get access to the Default View of that data table through currency manager that would listen to the updates to the underlying data table. The benefit of using CurrencyManager is that all the form elements would be kept synchronized.

Using CurrencyManager may cause performance issues in certain scenarios. In such cases, it can bypass CurrencyManager and access the list directly without ever involving it by setting BindToCurrencyManager property to false (which is true by default). This will in turn detach the grid from CurrencyManager and hence the grid engine does not register the list with Windows Forms Currency Manager and it solely relies on listening to ListChanged events.

// Specifies list to be attached to Currency Manager
this.gridGroupingControl1.BindToCurrencyManager = true;
' Specifies list to be attached to Currency Manager
Me.gridGroupingControl1.BindToCurrencyManager = True

Caching the Record Values

If the custom collections are assigned to the grid, user can choose to have the engine to cache record values using CacheRecordValues property. When set to true, the engine will cache copies of old values from a record in the record object. User can get these values withRecord.GetOldValue method. With custom collections, the engine can also determine exactly which values in a record were changed when the engine receives the ListChanged event and previous values were cached.

// Specifies list to be attached to Currency Manager
this.gridGroupingControl1.BindToCurrencyManager = true;
' Specifies list to be attached to Currency Manager
Me.gridGroupingControl1.BindToCurrencyManager = True

Maintaining the Counters

It specifies the CounterLogic to be used within the engine. GroupingEngine maintains counters for VisibleColumns, FilteredRecords, YAmount, HiddenRows and similar properties. These counters occupy a countable portion of the grid tree in memory. On every list change, all these counters need to be refreshed along with the data records.

Invalidating all the counters is not required at all times. For instance, if you have a larger data source and you do not want support for groups and filtered records, then there is no need to maintain counters such as FilteredRecords and similar properties. Keeping all the counters in memory will greatly increase memory consumption, which is not necessary in this case and there will degradation of grid performance.

To handle such scenarios, GridGroupingControl provides options to skip allocating these counters. By setting this property, it can reduce the memory footprint by selectively disabling the counters that are not required for the application. EngineCounters enum defines the values for this property, which will be discussed in the next chapter.

// All counters are supported
this.gridGroupingControl1.CounterLogic = Syncfusion.Grouping.EngineCounters.All;
' All counters are supported
Me.gridGroupingControl1.CounterLogic = Syncfusion.Grouping.EngineCounters.All

Inserting and Removing Records Behavior

InsertRemoveBehavior and InsertRemoveBehaviorWithEndEdit properties determine how the grid should react when a record is inserted or deleted. One or multiple records need to be shifted up and down. By default, the whole display is invalidated and all the rows are repainted though only one record needs to be redrawn.

By setting properties to ListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate, you can instruct the engine not to repaint the whole screen. The engine will now determine the area affected by this change and use ScrollWindow API to shift records up and down, and only repaint one record that was really changed. This will have a big impact if the grid has a large records and repainting the whole display is expensive.

// Invalidates All when records are inserted or removed
this.gridGroupingControl1.InsertRemoveBehavior = Syncfusion.Windows.Forms.Grid.Grouping.GridListChangedInsertRemoveBehavior.InvalidateAll;
' Invalidates All when records are inserted or removed
Me.gridGroupingControl1.InsertRemoveBehavior = Syncfusion.Windows.Forms.Grid.Grouping.GridListChangedInsertRemoveBehavior.InvalidateAll

Invalidating Layout on Record Changing

InvalidateAllWhenListChanged is used to specify whether the grid should simply call Invalidate when a ListChanged event is handled or if it should determine the area that is affected by the change and call InvalidateRange.

At first, it will look better to determine the area that is affected by a change and call InvalidateRange method. But when calling this method, the grid needs to know the exact position of the record in the table before it can mark that area dirty. In order to determine the record position (and y-position of the row in the display), counters need to be evaluated. This operation can cost more time than simply calling Invalidate in high-frequency update scenarios. The group caption bar also needs to be updated when a record changes.

InsertRemoveBehavior, SortPositionChangedBehavior properties and UpdateDisplayFrequency can speed up things when InvalidateAllWhenListChanged is set to false.

// Invalidates when ListChanged event is handled
this.gridGroupingControl1.InvalidateAllWhenListChanged = true;
' Invalidates when ListChanged event is handled
Me.gridGroupingControl1.InvalidateAllWhenListChanged = True

Raising ListChanged event only on Engine

When the engine handles ListChanged event, it will raise numerous events by itself. When RaiseSourceListChangedEventsOnEngineOnly property is set to true, the events will only be raised on the Engine object. If set to false then events will also be raised on inner objects (will bubble up on nested tables, which causes some performance overhead). It will only have effect if UseOldListChangedHandler is set to false.

// Events will be raised only on the Engine object
this.gridGroupingControl1.RaiseSourceListChangedEventsOnEngineOnly = true;
' Events will be raised only on the Engine object
Me.gridGroupingControl1.RaiseSourceListChangedEventsOnEngineOnly = True

Update on Sort Position Changed

SortPositionChangedBehavior and SortPositionChangedBehaviorWithEndEdit properties determine how to update display with a change in the sort position of a record. By default, the whole display is invalidated and all the rows are repainted though only one record needs to be redrawn. This will degrade performance and will not be efficient if grid have a larger data and repainting the whole display is expensive.

By setting these properties to ListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate, user can instruct the engine not to repaint the whole screen. This will only repaint the record that was really changed.

Update Frequency on particular time

UpdateDisplayFrequency property is used to specify the number of milliseconds to wait between display updates when new ListChanged event handler logic is used. This property does not have any effect if UseOldListChangedHandler is set to true. Special values are 0 - only manually update display by calling grid.Update() and 1 - update display immediately after each change.

Using Default Graphics for faster Drawing

By setting the UseDefaultsForFasterDrawing value as true, user can quickly switch to faster GDI draw text, solid borders and more efficient calculation of the optimal width of a column. Initializes recommended settings to improve handling of ListChanged events and scrolling through grid. Affected settings are: TableOptions.ColumnsMaxLengthStrategy, TableOptions.GridLineBorder, TableOptions.DrawTextWithGdiInterop, TableOptions.VerticalPixelScroll, Appearance.AnyRecordFieldCell.WrapText and Appearance.AnyRecordFieldCell.Trimming.

// Improves handling of ListChanged events and scrolling
this.gridGroupingControl1.UseDefaultsForFasterDrawing = true;
' Improves handling of ListChanged events and scrolling
Me.gridGroupingControl1.UseDefaultsForFasterDrawing = True

Using Old ListChanged event

With version 4.4, the engine changed the way ListChanged event is handled internally to fix shortcomings with performance of the code that was in place earlier. UseOldListChangedHandler property is used to switch back the behavior of the engine to old mechanism in case of notice any compatibility issues. The default value is false.

// Switch backs to the old behavior
this.gridGroupingControl1.UseOldListChangedHandler = true;
' Switch backs to the old behavior
Me.gridGroupingControl1.UseOldListChangedHandler = True

Blinking Time

GridGroupingControl has in-built support for highlighting cells for a short period of time after a change is detected to a cell. The BlinkTime property lets the user to specify the amount of time in milliseconds for the values should be highlighted. User can also enable or disable this feature for individual columns by toggling GridColumnDescriptor.AllowBlink property.

// Sets the time in milliseconds while blinking
this.gridGroupingControl1.BlinkTime = 500;
' Sets the time in milliseconds while blinking
Me.gridGroupingControl1.BlinkTime = 500

Memory Performance – Engine Optimizations

Engine optimizations will greatly help improve Memory Performance. Triggering these optimizations selectively will help in reducing memory footprint.

Optimizing Memory performance

Engine optimizations can be enabled by setting AllowedOptimizations to some value other than None. To optimize memory usage, CounterLogic property needs to be assigned with a proper value.

The user can specify the Engine optimizations by setting the AllowedOptimizations value as desired optimizations. The following is the list of optimizations the grid offers that are defined by EngineOptimizations enumeration. By default, they are turned off, but the user can tell the engine which optimization should be applied when the specified criteria for the optimization is met.

// Enable Engine optimizations
this.gridGroupingControl1.AllowedOptimizations = Syncfusion.Grouping.EngineOptimizations.All;
' Enable Engine optimizations
Me.gridGroupingControl1.AllowedOptimizations = Syncfusion.Grouping.EngineOptimizations.All

The followings are the EngineOptimizations enumerations available for AllowedOptimizations,

None - All optimizations are disabled.

DisableCounters - When the engine detects that a table does not have RecordFilters, GroupedColumns, or nested relations, counter logic will be disabled for RecordsDetails collection since all counters are in sync with actual records (e.g. all records in data source are shown in TopLevelGroup). With this optimization, the engine will still have full support for sorting.

VirtualMode - When all criteria are met for optimization and in addition to that no SortedColumns is set, RecordsDetails collection does not have to be initialized at all. Instead, it can create records elements on demand and discard them using regular garbage collection when no references to a Record exists any more (e.g. once you scroll them out of view). This approach reduces memory footprint to absolute minimum. It allows to load and display millions of records in a table. The PrimaryKey collection is still supported, but it will be initialized only on demand if it is accessed via Table.PrimaryKeySortedRecords collection. In such case all records will be enumerated.

PassThroughSort - When all criteria are met for optimization and SortedColumns are set, the engine will normally have to loop through records and sort them. When the user specifies, the engine will check if the data source is an IBindingList and if IBindingList.SupportsSort returns true. In such cases, data source will be sorted by using IBindingList.Sort routine and the engine will access records by using VirtualMode. Using IBindingList is usually a bit faster than the engines own sorting routines, but the disadvantage is that it will lose CurrentRecord and SelectedRecords information. Inserting and removing records will also be slower (especially if the underlying data source is DataView). EngineOptimizations.PassThroughSort will be ignored if criteria are met for the optimization are not met. If the user wants to force a Pass-through sort mechanism in such a case, it can be done by implementingIGroupingList interface. This allows performing the sort on data view directly instead of letting grouping engine perform the sorting. Normally, it is recommended to use the engine’s own Sort mechanism and only rely on EngineOptimizations.PassThroughSort for virtual mode scenarios.

RecordsAsDisplayElements - When the engine detects that records do not have nested child tables, no record preview rows are being used, and each record has only one row (no ColumnSets are used), records do not have to be split into RecordParts. Instead when querying DisplayElements collection for a specific row, the engine can simply return Record element instead of RecordRow element. The same applies to CaptionSection, ColumnHeaderSection and FilterBarSection. Instead of returning CaptionRow, ColumnHeaderRow, or FilterBarRow element, DisplayElements collection returns section element. While using this optimization, need to be careful on querying the DisplayElements collection instead of RecordRow element, Record element can be returned. So also with ColumnHeader, FilterBase, and Caption.

All – It will enable the optimizations - DisableCounters, VirtualMode, and RecordsAsDisplayElements. Based on the schema that had specify, the engine will determine if certain optimizations can be applied. If it’s a flat table and do not sort records, VirtualMode will be applied and records do not have to be touched at all (only when drawing). If the records are sorted, then tree tables will be built so that the grid can sort records, but the logic for filtering and grouping is turned off (DisableCounters optimization). In case of pass-through sorting, the table is sorted by using DataView.Sort routine and records will be accessed with VirtualMode. If the Grid have group records or nested tables, then full grouping logic will be needed.

Counter Logic - In addition to specify VirtualMode or WithoutCounter mode, users can also specify the counters as per need. Most of the times the user only wants to count visible elements and filtered records and the others can leave out like custom counters, hidden element counter, etc. That can save a few bytes per record (40-80 bytes). The engine will also determine whether records actually need to be broken into pieces or if a record can be returned as leave elements (RecordsAsDisplayElements option). This again saves a few bytes per record.

// Set the CounterLogic for the Engine.
this.gridGroupingControl1.CounterLogic = Syncfusion.Grouping.EngineCounters.FilteredRecords;
' Set the CounterLogic for the Engine.
Me.gridGroupingControl1.CounterLogic = Syncfusion.Grouping.EngineCounters.FilteredRecords

The EngineCounters enumeration properties available for specify the CounterLogic are listed as follows,

All - All counters are supported: visible elements, filtered records, YAmount, hidden elements, hidden records, CustomCount and VisibleCustomCount. Highest memory footprint.

FilteredRecords - Counts only visible elements and filtered records. Smallest memory footprint.

YAmount - Counts visible elements, filtered records and YAmount. Medium memory footprint.

NOTE

Allowing certain optimizations does not mean that the optimization is necessarily used. Optimizations will only be used when applicable. Take for example the optimization. While enable this optimization, the engine will check schema settings when loading the table. If there are no SortedColumns, RecordFilters, GroupedColumns, or nested relations for a table, then virtual mode can be used and no records need to be loaded into memory. If the user later sorts by one column, the virtual mode cannot be used any more. Records will need to be iterated through and sorted and tree structures will be built that allow quick access to records and IndexOf operations. When initializing table, the engine will check if criteria for DisableCounters optimization are met.

Implementation

All the optimizations are enabled by setting AllowedOptimizations to All. As said earlier, optimizations specified will not be applied at all times. They will only be used when applicable, that is, when the criteria for those optimizations are met. This example best illustrates this process. On every property change, the log window displays the list of optimizations applied to the grid at that instance. While running the sample, it can track the optimizations applied in different engine states like with or without grouping, with or without sorting, etc.

Sample Location:

<Installed_Location>\Syncfusion\EssentialStudio\<Version_Number>\Windows\Grid.Grouping.Windows\Samples\Performance\Engine Optimization Demo\CS

Use the following steps to experiment different engine optimizations.
1.Create a class (VirtualItem) that represents the record structure. Its data members form the record fields.

public class VirtualItem
{
    int index;
    string name;
    double someValue;
    double otherValue;

    public int Index
    {
        get
        {
            return index;
        }
        set
        {
            index = value;
        }
    }

    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
        }
    }

    public double SomeValue
    {
        get
        {
            return someValue;
        }
        set
        {
            someValue = value;
        }
    }

    public double OtherValue
    {
        get
        {
            return otherValue;
        }
        set
        {
            otherValue = value;
        }
    }

}
Public Class VirtualItem
    Private index_Renamed As Integer
    Private name_Renamed As String
    Private someValue_Renamed As Double
    Private otherValue_Renamed As Double

    Public Property Index() As Integer
        Get
            Return index_Renamed
        End Get
        Set(ByVal value As Integer)
            index_Renamed = value
        End Set
    End Property

    Public Property Name() As String
        Get
            Return name_Renamed
        End Get
        Set(ByVal value As String)
            name_Renamed = value
        End Set
    End Property

    Public Property SomeValue() As Double
        Get
            Return someValue_Renamed
        End Get
        Set(ByVal value As Double)
            someValue_Renamed = value
        End Set
    End Property

    Public Property OtherValue() As Double
        Get
            Return otherValue_Renamed
        End Get
        Set(ByVal value As Double)
            otherValue_Renamed = value
        End Set
    End Property

End Class

2.Create another class (VirtualList) by implementingIList and ITypedList interfaces. This class represents your collection that serves as data source for grid grouping control. Refer to CustomCollections under the DataBinding topic to know how to implement these interfaces.

public class VirtualList : IList, ITypedList
{
    int virtualCount;

    public VirtualList(int count)
    {
        virtualCount = count;
    }

    #region IList Members

    public bool IsReadOnly
    {
        get
        {
            return true;
        }
    }

    public object this[int index]
    {
        get
        {
            VirtualItem item = new VirtualItem();
            item.Index = index;
            item.Name = "Name" + index.ToString("000000000");
            item.SomeValue = index * 0.873332f;
            item.OtherValue = (293023033 - index) / 8;

            return item;
        }
        set
        {
        }
    }

    public void RemoveAt(int index)
    {
    }

    public void Insert(int index, object value)
    {
    }

    public void Remove(object value)
    {
    }

    public bool Contains(object value)
    {
        return false;
    }

    public void Clear()
    {
    }

    public int IndexOf(object value)
    {
        return 0;
    }

    public int Add(object value)
    {
        return 0;
    }

    public bool IsFixedSize
    {
        get
        {
            return true;
        }
    }

    #endregion

    #region ICollection Members

    public bool IsSynchronized
    {
        get
        {
            return false;
        }
    }

    public int Count
    {
        get
        {
            return virtualCount;
        }
    }

    public void CopyTo(Array array, int index)
    {
    }

    public object SyncRoot
    {
        get
        {
            return null;
        }
    }

    #endregion

    #region IEnumerable Members

    public IEnumerator GetEnumerator()
    {
        return null;
    }

    #endregion

    #region ITypedList Members

    public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
    {
        System.ComponentModel.PropertyDescriptorCollection propertyDescriptorCollection
            = TypeDescriptor.GetProperties(typeof(VirtualItem));

        string[] attr = new string[]
        {
            "Index",
            "Name",
            "SomeValue",
            "OtherValue",
        };

        return propertyDescriptorCollection.Sort(attr);
    }

    public string GetListName(PropertyDescriptor[] listAccessors)
    {
        return "VirtualList";
    }

    #endregion
}
Public Class VirtualList
    Implements IList, ITypedList
    Private virtualCount As Integer

    Public Sub New(ByVal count As Integer)
        virtualCount = count
    End Sub

    #Region "IList Members"

    Public ReadOnly Property IsReadOnly() As Boolean
        Get
            Return True
        End Get
    End Property

    Default Public Property Item(ByVal index As Integer) As Object
        Get
            Dim item As New VirtualItem()
            item.Index = index
            item.Name = "Name" & index.ToString("000000000")
            item.SomeValue = index * 0.873332f
            item.OtherValue = (293023033 - index) / 8

            Return item
        End Get
        Set(ByVal value As Object)
        End Set
    End Property

    Public Sub RemoveAt(ByVal index As Integer)
    End Sub

    Public Sub Insert(ByVal index As Integer, ByVal value As Object)
    End Sub

    Public Sub Remove(ByVal value As Object)
    End Sub

    Public Function Contains(ByVal value As Object) As Boolean
        Return False
    End Function

    Public Sub Clear()
    End Sub

    Public Function IndexOf(ByVal value As Object) As Integer
        Return 0
    End Function

    Public Function Add(ByVal value As Object) As Integer
        Return 0
    End Function

    Public ReadOnly Property IsFixedSize() As Boolean
        Get
            Return True
        End Get
    End Property

    #End Region

    #Region "ICollection Members"

    Public ReadOnly Property IsSynchronized() As Boolean
        Get
            Return False
        End Get
    End Property

    Public ReadOnly Property Count() As Integer
        Get
            Return virtualCount
        End Get
    End Property

    Public Sub CopyTo(ByVal array As Array, ByVal index As Integer)
    End Sub

    Public ReadOnly Property SyncRoot() As Object
        Get
            Return Nothing
        End Get
    End Property

    #End Region

    #Region "IEnumerable Members"

    Public Function GetEnumerator() As IEnumerator
        Return Nothing
    End Function

    #End Region

    #Region "ITypedList Members"

    Public Function GetItemProperties(ByVal listAccessors() As PropertyDescriptor) As PropertyDescriptorCollection
        Dim propertyDescriptorCollection As System.ComponentModel.PropertyDescriptorCollection = TypeDescriptor.GetProperties(GetType(VirtualItem))

        Dim attr() As String = { "Index", "Name", "SomeValue", "OtherValue" }

        Return propertyDescriptorCollection.Sort(attr)
    End Function

    Public Function GetListName(ByVal listAccessors() As PropertyDescriptor) As String
        Return "VirtualList"
    End Function

    #End Region
End Class

3.Add a button and ListBox to the main form. Clicking the button will create a grid grouping control and load it with Virtual List. ListBox serves as Log Window wherein user will display the log messages like time elapsed for loading the grid, list of optimizations applied, and so on. The form will be look like the one below at design time.
Performance_img2

4.Set up a new engine and specify the optimizations settings required.

GridEngine schema = new GridEngine();
schema.InvalidateAllWhenListChanged = false;
schema.AllowedOptimizations = EngineOptimizations.All;
schema.CounterLogic = EngineCounters.YAmount;

//Also dependent on CounterLogic = EngineCounters.YAmount.
schema.TableOptions.VerticalPixelScroll = true;
schema.TableOptions.ColumnsMaxLengthStrategy = GridColumnsMaxLengthStrategy.FirstNRecords;
schema.TableOptions.ColumnsMaxLengthFirstNRecords = 100;
schema.TableOptions.AllowSortColumns = true;
schema.TableDescriptor.AllowEdit = false;
schema.DataSource = new VirtualList(100000);
schema.Reset();
schema.TableDescriptor.Columns["Index"].MaxLength = 10;
Dim schema As New GridEngine()
schema.InvalidateAllWhenListChanged = False
schema.AllowedOptimizations = EngineOptimizations.All
schema.CounterLogic = EngineCounters.YAmount

'Also dependent on CounterLogic = EngineCounters.YAmount.
schema.TableOptions.VerticalPixelScroll = True
schema.TableOptions.ColumnsMaxLengthStrategy = GridColumnsMaxLengthStrategy.FirstNRecords
schema.TableOptions.ColumnsMaxLengthFirstNRecords = 100
schema.TableOptions.AllowSortColumns = True
schema.TableDescriptor.AllowEdit = False
schema.DataSource = New VirtualList(100000)
schema.Reset()
schema.TableDescriptor.Columns("Index").MaxLength = 10

5.Define a method LogMemoryUsage that calculates the amount of memory consumed and displays various optimizations applied to the grouping engine.

void LogMemoryUsage()
{

    //Forces garbage collection and gets used memory size.
    GC.Collect();
    System.Threading.Thread.Sleep(10);
    GC.Collect();
    System.Threading.Thread.Sleep(100);
    GC.Collect();
    LogWindow.Items.Add(string.Format("Optimizations for {0}: ", this.gridGroupingControl1.TableDescriptor.Name));
    LogWindow.Items.Add(string.Format("VirtualMode: {0}, ", this.gridGroupingControl1.Table.VirtualMode));
    LogWindow.Items.Add(string.Format("WithoutCounter: {0}, ", this.gridGroupingControl1.Table.WithoutCounter));
    LogWindow.Items.Add(string.Format("RecordsAsDisplayElements: {0}, ", gridGroupingControl1.Table.RecordsAsDisplayElements));
    Process myProcess = Process.GetCurrentProcess();
    double workingSetSizeInKiloBytes = myProcess.WorkingSet64 / 1000;
    string s = "Process's physical memory usage: " + workingSetSizeInKiloBytes.ToString() + " kb";
    LogWindow.Items.Add(s); 
    LogWindow.Items.Add("");
}
Private Sub LogMemoryUsage()

    'Forces garbage collection and gets used memory size.
    GC.Collect()
    System.Threading.Thread.Sleep(10)
    GC.Collect()
    System.Threading.Thread.Sleep(100)
    GC.Collect()
    LogWindow.Items.Add(String.Format("Optimizations for {0}: ", Me.gridGroupingControl1.TableDescriptor.Name))
    LogWindow.Items.Add(String.Format("VirtualMode: {0}, ", Me.gridGroupingControl1.Table.VirtualMode))
    LogWindow.Items.Add(String.Format("WithoutCounter: {0}, ", Me.gridGroupingControl1.Table.WithoutCounter))
    LogWindow.Items.Add(String.Format("RecordsAsDisplayElements: {0}, ", gridGroupingControl1.Table.RecordsAsDisplayElements))
    Dim myProcess As Process = Process.GetCurrentProcess()
    Dim workingSetSizeInKiloBytes As Double = myProcess.WorkingSet64 \ 1000
    Dim s As String = "Process's physical memory usage: " & workingSetSizeInKiloBytes.ToString() & " kb"
    LogWindow.Items.Add(s)
    LogWindow.Items.Add("")
End Sub

6.Handle the ButtonClick event in order to populate the grid when the button is clicked. It also calls LogMemoryUsage method to display initial optimization settings for the grid - the optimizations for an ungrouped and unsorted grid.

this.buttonLoadGrid.Click += new System.EventHandler(this.LoadGridLoadGrid_Click);
private void LoadGridLoadGrid_Click(object sender, EventArgs e)
{
    this.LogWindow.Items.Add("");
    this.LogWindow.Items.Add("Flat, Virtual List with Sorting and Grouping Enabled.");
    int time = Environment.TickCount;
    Cursor.Current = Cursors.WaitCursor;

    //Loads a Grid Grouping control with a new engine.
    gridGroupingControl1 = new GridGroupingControl();
    gridGroupingControl1.BackColor = System.Drawing.SystemColors.Window;
    gridGroupingControl1.Dock = System.Windows.Forms.DockStyle.Fill;
    gridGroupingControl1.Name = "gridGroupingControl1";
    gridGroupingControl1.TabIndex = 0;
    gridGroupingControl1.IntelliMousePanning = true;
    this.panel1.Controls.Add(this.gridGroupingControl1);
    gridGroupingControl1.Engine = schema;
    gridGroupingControl1.DataSource = new VirtualList(100000);
    gridGroupingControl1.ShowGroupDropArea = true;
    this.Refresh();
    Cursor.Current = Cursors.Arrow;
    this.LogWindow.Items.Add(string.Format("Elapsed Time: {0}", Environment.TickCount - time));
    gridGroupingControl1.Appearance.AnyCell.Font.Facename = "Verdana";
    gridGroupingControl1.Appearance.AnyCell.TextColor = Color.MidnightBlue;
    gridGroupingControl1.TableOptions.GridVisualStyles = Syncfusion.Windows.Forms.GridVisualStyles.Office2007Blue;
    gridGroupingControl1.TableOptions.GridLineBorder = new GridBorder(GridBorderStyle.Solid, Color.FromArgb(227, 239, 255), GridBorderWeight.Thin);

    //Initial Log Display.
    LogMemoryUsage();
}
AddHandler buttonLoadGrid.Click, AddressOf LoadGridLoadGrid_Click
Private Sub LoadGridLoadGrid_Click(ByVal sender As Object, ByVal e As EventArgs)
    Me.LogWindow.Items.Add("")
    Me.LogWindow.Items.Add("Flat, Virtual List with Sorting and Grouping Enabled.")
    Dim time As Integer = Environment.TickCount
    Cursor.Current = Cursors.WaitCursor

    'Loads a Grid Grouping control with a new engine.
    gridGroupingControl1 = New GridGroupingControl()
    gridGroupingControl1.BackColor = System.Drawing.SystemColors.Window
    gridGroupingControl1.Dock = System.Windows.Forms.DockStyle.Fill
    gridGroupingControl1.Name = "gridGroupingControl1"
    gridGroupingControl1.TabIndex = 0
    gridGroupingControl1.IntelliMousePanning = True
    Me.panel1.Controls.Add(Me.gridGroupingControl1)
    gridGroupingControl1.Engine = schema
    gridGroupingControl1.DataSource = New VirtualList(100000)
    gridGroupingControl1.ShowGroupDropArea = True
    Me.Refresh()
    Cursor.Current = Cursors.Arrow
    Me.LogWindow.Items.Add(String.Format("Elapsed Time: {0}", Environment.TickCount - time))
    gridGroupingControl1.Appearance.AnyCell.Font.Facename = "Verdana"
    gridGroupingControl1.Appearance.AnyCell.TextColor = Color.MidnightBlue
    gridGroupingControl1.TableOptions.GridVisualStyles = Syncfusion.Windows.Forms.GridVisualStyles.Office2007Blue
    gridGroupingControl1.TableOptions.GridLineBorder = New GridBorder(GridBorderStyle.Solid, Color.FromArgb(227, 239, 255), GridBorderWeight.Thin)

    'Initial Log Display.
    LogMemoryUsage()
End Sub

7.Handle PropertyChanging event to display log for every property that is being changed in the grid. This will be raised when you group or sort the grid grouping control and hence you could track the results of these operations (especially the current optimizations) here.

gridGroupingControl1.PropertyChanging += new DescriptorPropertyChangedEventHandler(gridGroupingControl1_PropertyChanging);
Timer t = null;

void gridGroupingControl1_PropertyChanging(object sender, DescriptorPropertyChangedEventArgs e)
{
    LogWindow.Items.Add(e.ToString());

    if (t != null)
    {
        t.Tick -= new EventHandler(t_Tick);
        t.Dispose();
    }
    t = new Timer();
    t.Interval = 100;
    t.Tick += new EventHandler(t_Tick);
    t.Start();
}

private void t_Tick(object sender, EventArgs e)
{
    Timer t = (Timer)sender;
    t.Tick -= new EventHandler(t_Tick);
    t.Dispose();
    this.LogMemoryUsage();
}
AddHandler gridGroupingControl1.PropertyChanging, AddressOf gridGroupingControl1_PropertyChanging
Private t As Timer = Nothing

Private Sub gridGroupingControl1_PropertyChanging(ByVal sender As Object, ByVal e As DescriptorPropertyChangedEventArgs)
    LogWindow.Items.Add(e.ToString())

    If t IsNot Nothing Then
        RemoveHandler t.Tick, AddressOf t_Tick
        t.Dispose()
    End If
    t = New Timer()
    t.Interval = 100
    AddHandler t.Tick, AddressOf t_Tick
    t.Start()
End Sub

Private Sub t_Tick(ByVal sender As Object, ByVal e As EventArgs)
    Dim t As Timer = CType(sender, Timer)
    RemoveHandler t.Tick, AddressOf t_Tick
    t.Dispose()
    Me.LogMemoryUsage()
End Sub

Performance_img3

Record ListChanged Performance

When ListChanged is detected, the grouping engine has to update the grid records accordingly. Every record change may affect its sort position, group dependency, and summaries. The engine should take care of all these things and should also invalidate counters that are being affected with respect to ListChanged. The easiest way to accomplish this would be invalidating the whole display and repainting all the rows. But this will have a big impact on grid performance in worst cases. For example, in case only one record is really changed and this change does not affect sort order and summaries, it requires to repaint only one record. Instead the engine will repaint the whole display.

GridEngine provides options to handle this type of scenarios and by using those it will track the expression fields and summary columns that depend on changes to a field, fields affecting group dependency or sort position. Based on these findings, it will choose the most efficient way to update the engine’s internal object to keep up with the ListChanged events.

Grid can be optimized by using the GridListChangedInsertRemoveBehavior enumeration while add/remove and sorting the grid records. It has the following values to set InsertRemoveBehavior and SortPositionChangedBehavior.

InvalidateVisible - It will keep engine in synchronization with ListChanged notifications and then invalidate rows on screen, below affected row.

InvalidateAll - It will simply set TableDirty value as true and the engine will not try to keep anything in synchronization at that time.

ScrollWithImmediateUpdate - It will keep engine in synchronization and use ScrollWindow to scroll window contents or adjust top row index if changes occurred before current visible row.

Implementation

The implementation uses custom summary class named ManualTotalSummary. This is a manual summary class, which can be updated immediately using the difference between old and new value in ListChanged event. The Total property calculates summaries for groups and table manually by looping through each group and record, and summing up the values of changed field. It provides faster updates on summaries by applying a delta between the old and new value when a record is changed.

Sample Location:

<Installed_Location>\Syncfusion\EssentialStudio\<Version_Number>\Windows\Grid.Grouping.Windows\Samples\Performance\Manual Total Summary Demo

Use the following steps to improve the performance,

public class ManualTotalSummary 
{
    double total;
    bool dirty = true;
    Group group;
    int fieldIndex = -1;

    public ManualTotalSummary(Group g, string field)
        : this(g, g.ParentTableDescriptor.Fields[field])
    {
    }

    public ManualTotalSummary(Group g, FieldDescriptor field)
    {
        this.Field = field;
        this.Group = g;
    }

    public int FieldIndex
    {
        set
        {
            fieldIndex = value;
        }
        get
        {

            if (fieldIndex == -1)
                fieldIndex = field.Collection.IndexOf(Field);
            return this.fieldIndex;
        }
    }

    public Group Group
    {
        get
        {
            return this.group;
        }
        set
        {
            this.group = value;
            IManualTotalSummaryArraySource ts = group as IManualTotalSummaryArraySource;
            ts.GetManualTotalSummaryArray()[FieldIndex] = this;
        }
    }
    FieldDescriptor field;

    public FieldDescriptor Field
    {
        get
        {
            return this.field;
        }
        set
        {
            this.field = value;
        }
    }

    public bool Dirty
    {
        get
        {
            return this.dirty;
        }
        set
        {
            this.dirty = value;
        }
    }

    public double Total
    {
        get
        {
            if (dirty)
            {
                CalculateTotal();
                this.dirty = false;
            }
            return this.total;
        }
        set
        {
            this.total = value;
        }
    }

    void CalculateTotal()
    {
        total = 0;

        if (group.Details is RecordsDetails)
        {

            foreach (Record r in group.Records)
            {
                object obj = r.GetValue(field);

                if (obj != null && !(obj is DBNull))
                {
                    double d = Convert.ToDouble(obj);
                    total += d;
                }
            }
        }

        else
        {

            foreach (Group g in group.Groups)
            {
                IManualTotalSummaryArraySource ts = g as IManualTotalSummaryArraySource;
                ManualTotalSummary mt = ts.GetManualTotalSummaryArray()[this.FieldIndex];

                if (mt == null)
                    mt = new ManualTotalSummary(g, field);
                double d = mt.Total;
                total += d;
            }
        }
    }

    public void ApplyDelta(Element r, bool isObsoleteRecord, bool isAddedRecord, ChangedFieldInfo fieldInfo)
    {
 
        if (Dirty)
            return;
       ManualTotalSummary mt = this;

        if (isObsoleteRecord)
        {

            if (fieldInfo.OldValue != null && !(fieldInfo.OldValue is DBNull))
                mt.Total -= Convert.ToDouble(fieldInfo.OldValue);
        }

        else if (isAddedRecord)
        {

            if (fieldInfo.NewValue != null && !(fieldInfo.NewValue is DBNull))
                mt.Total += Convert.ToDouble(fieldInfo.NewValue);
        }

        else
            mt.Total += fieldInfo.Delta;
    }

}
Public Class ManualTotalSummary
    Private total_Renamed As Double
    Private dirty_Renamed As Boolean = True
    Private group_Renamed As Group
    Private fieldIndex_Renamed As Integer = -1

    Public Sub New(ByVal g As Group, ByVal field As String)
        Me.New(g, g.ParentTableDescriptor.Fields(field))
    End Sub

    Public Sub New(ByVal g As Group, ByVal field As FieldDescriptor)
        Me.Field = field
        Me.Group = g
    End Sub

    Public Property FieldIndex() As Integer
        Set(ByVal value As Integer)
            fieldIndex_Renamed = value
        End Set
        Get

            If fieldIndex_Renamed = -1 Then
                fieldIndex_Renamed = field_Renamed.Collection.IndexOf(Field)
            End If
            Return Me.fieldIndex_Renamed
        End Get
    End Property

    Public Property Group() As Group
        Get
            Return Me.group_Renamed
        End Get
        Set(ByVal value As Group)
            Me.group_Renamed = value
            Dim ts As IManualTotalSummaryArraySource = TryCast(group_Renamed, IManualTotalSummaryArraySource)
            ts.GetManualTotalSummaryArray()(FieldIndex) = Me
        End Set
    End Property

    Private field_Renamed As FieldDescriptor

    Public Property Field() As FieldDescriptor
        Get
            Return Me.field_Renamed
        End Get
        Set(ByVal value As FieldDescriptor)
            Me.field_Renamed = value
        End Set
    End Property

    Public Property Dirty() As Boolean
        Get
            Return Me.dirty_Renamed
        End Get
        Set(ByVal value As Boolean)
            Me.dirty_Renamed = value
        End Set
    End Property

    Public Property Total() As Double
        Get

            If dirty_Renamed Then
                CalculateTotal()
                Me.dirty_Renamed = False
            End If
            Return Me.total_Renamed
        End Get
        Set(ByVal value As Double)
            Me.total_Renamed = value
        End Set
    End Property

    Private Sub CalculateTotal()
        total_Renamed = 0

        If TypeOf group_Renamed.Details Is RecordsDetails Then

            For Each r As Record In group_Renamed.Records
                Dim obj As Object = r.GetValue(field_Renamed)

                If obj IsNot Nothing AndAlso Not(TypeOf obj Is DBNull) Then
                    Dim d As Double = Convert.ToDouble(obj)
                    total_Renamed += d
                End If
            Next r
        Else

            For Each g As Group In group_Renamed.Groups
                Dim ts As IManualTotalSummaryArraySource = TryCast(g, IManualTotalSummaryArraySource)
                Dim mt As ManualTotalSummary = ts.GetManualTotalSummaryArray()(Me.FieldIndex)

                If mt Is Nothing Then
                    mt = New ManualTotalSummary(g, field_Renamed)
                End If
                Dim d As Double = mt.Total
                total_Renamed += d
            Next g
        End If
    End Sub

    Public Sub ApplyDelta(ByVal r As Element, ByVal isObsoleteRecord As Boolean, ByVal isAddedRecord As Boolean, ByVal fieldInfo As ChangedFieldInfo)

        If Dirty Then
            Return
        End If
        Dim mt As ManualTotalSummary = Me

        If isObsoleteRecord Then

            If fieldInfo.OldValue IsNot Nothing AndAlso Not(TypeOf fieldInfo.OldValue Is DBNull) Then
                mt.Total -= Convert.ToDouble(fieldInfo.OldValue)
            End If

        ElseIf isAddedRecord Then

            If fieldInfo.NewValue IsNot Nothing AndAlso Not(TypeOf fieldInfo.NewValue Is DBNull) Then
                mt.Total += Convert.ToDouble(fieldInfo.NewValue)
            End If

        Else
            mt.Total += fieldInfo.Delta
        End If
    End Sub

End Class

2.ManualTotalSummary class makes use of ManualTotalSummaryTable class, which derives GridTable to calculate the new total. ManualTotalSummaryTable class overrides OnRecordChanged event in order to track record changes and keeps track of old and new values of the ChangedField. For each entry in ManualTotalSummaryTable.TotalSummaries, a ManualTotalSummary will be created.

public class ManualTotalSummaryTable : GridTable
{
    public ManualTotalSummaryTable(TableDescriptor tableDescriptor, Table parentRelationTable)
        : base((GridTableDescriptor)tableDescriptor, (GridTable)parentRelationTable)
    {
    }

    #region TotalSummaries Support
    ArrayList totalSummaries = new ArrayList();

    public ArrayList TotalSummaries
    {
        get
        {
            return this.totalSummaries;
        }
        set
        {
            this.totalSummaries = value;
        }
    }

    protected override void OnPrepareRemoving(object row)
    {

        // Save values for all fields where we need to be able to access the

        // old value (e.g. Delta for TotalSummaries).
        TableDescriptor td = TableDescriptor;
        IManualTotalSummaryArraySource ts = this.TopLevelGroup as IManualTotalSummaryArraySource;

        if (ts != null)
        {

            foreach (string name in this.totalSummaries)
            {
                FieldDescriptor fieldDescriptor = td.Fields[name];

                if (fieldDescriptor.IsPropertyField())
                {
                    PropertyDescriptor pd = fieldDescriptor.GetPropertyDescriptor();
                    object value = GetValue(row, pd);
                    ChangedFieldInfo fieldInfo = new ChangedFieldInfo(td, pd.Name, value, null);
                    this.AddChangedField(fieldInfo);
                }
            }
        }
        base.OnPrepareRemoving(row);
    }

    protected override void OnPrepareItemAdded(object row)
    {

        // Get new values for which delta information is needed
        IManualTotalSummaryArraySource ts = this.TopLevelGroup as IManualTotalSummaryArraySource;

        if (ts != null)
        {
            TableDescriptor td = TableDescriptor;

            foreach (string name in this.totalSummaries)
            {
                FieldDescriptor fieldDescriptor = td.Fields[name];

                if (fieldDescriptor.IsPropertyField())
                {
                    PropertyDescriptor pd = fieldDescriptor.GetPropertyDescriptor();
                    object value = GetValue(row, pd);
                    ChangedFieldInfo fieldInfo = new ChangedFieldInfo(td, pd.Name, null, value);
                    this.AddChangedField(fieldInfo);
                }
            }
        }
        base.OnPrepareItemAdded(row);
    }

    protected override void OnRecordChanged(Element r, bool isObsoleteRecord, bool isAddedRecord)
    {
        TableDescriptor td = TableDescriptor;
        Group g = r.ParentGroup;

        while (g is IManualTotalSummaryArraySource)
        {
            OnGroupSummaryInvalidated(new GroupEventArgs(g));

            IManualTotalSummaryArraySource ts = g as IManualTotalSummaryArraySource;

            foreach (ChangedFieldInfo fieldInfo in this.ChangedFieldsArray)
            {
                ManualTotalSummary mt = ts.GetManualTotalSummaryArray()[fieldInfo.FieldIndex];

                if (mt != null)
                    mt.ApplyDelta(r, isObsoleteRecord, isAddedRecord, fieldInfo);
            }
            g = g.ParentGroup;
        }
    } 

// Fix ManualTotalSummary of parent groups.
    #endregion
}
Public Class ManualTotalSummaryTable
    Inherits GridTable

    Public Sub New(ByVal tableDescriptor As TableDescriptor, ByVal parentRelationTable As Table)
        MyBase.New(CType(tableDescriptor, GridTableDescriptor), CType(parentRelationTable, GridTable))
    End Sub

#Region "TotalSummaries Support"

    Private totalSummaries_Renamed As New ArrayList()

    Public Property TotalSummaries() As ArrayList
        Get
            Return Me.totalSummaries_Renamed
        End Get
        Set(ByVal value As ArrayList)
            Me.totalSummaries_Renamed = value
        End Set
    End Property

    Protected Overrides Sub OnPrepareRemoving(ByVal row As Object)

        ' Save values for all fields where we need to be able to access the

        ' old value (e.g. Delta for TotalSummaries).
        Dim td As TableDescriptor = TableDescriptor
        Dim ts As IManualTotalSummaryArraySource = TryCast(Me.TopLevelGroup, IManualTotalSummaryArraySource)

        If ts IsNot Nothing Then

            For Each name As String In Me.totalSummaries_Renamed
                Dim fieldDescriptor As FieldDescriptor = td.Fields(name)

                If fieldDescriptor.IsPropertyField() Then
                    Dim pd As PropertyDescriptor = fieldDescriptor.GetPropertyDescriptor()
                    Dim value As Object = GetValue(row, pd)

                    Dim fieldInfo As New ChangedFieldInfo(td, pd.Name, value, Nothing)
                    Me.AddChangedField(fieldInfo)
                End If
            Next name
        End If
        MyBase.OnPrepareRemoving(row)
    End Sub

    Protected Overrides Sub OnPrepareItemAdded(ByVal row As Object)

        ' Get new values for which delta information is needed
        Dim ts As IManualTotalSummaryArraySource = TryCast(Me.TopLevelGroup, IManualTotalSummaryArraySource)

        If ts IsNot Nothing Then
            Dim td As TableDescriptor = TableDescriptor

            For Each name As String In Me.totalSummaries_Renamed
                Dim fieldDescriptor As FieldDescriptor = td.Fields(name)
                If fieldDescriptor.IsPropertyField() Then
                    Dim pd As PropertyDescriptor = fieldDescriptor.GetPropertyDescriptor()
                    Dim value As Object = GetValue(row, pd)
                    Dim fieldInfo As New ChangedFieldInfo(td, pd.Name, Nothing, value)
                    Me.AddChangedField(fieldInfo)
                End If
            Next name
        End If
        MyBase.OnPrepareItemAdded(row)
    End Sub

    Protected Overrides Sub OnRecordChanged(ByVal r As Element, ByVal isObsoleteRecord As Boolean, ByVal isAddedRecord As Boolean)
        Dim td As TableDescriptor = TableDescriptor
        Dim g As Group = r.ParentGroup

        Do While TypeOf g Is IManualTotalSummaryArraySource
            OnGroupSummaryInvalidated(New GroupEventArgs(g))
            Dim ts As IManualTotalSummaryArraySource = TryCast(g, IManualTotalSummaryArraySource)

            For Each fieldInfo As ChangedFieldInfo In Me.ChangedFieldsArray
                Dim mt As ManualTotalSummary = ts.GetManualTotalSummaryArray()(fieldInfo.FieldIndex)

                If mt IsNot Nothing Then
                    mt.ApplyDelta(r, isObsoleteRecord, isAddedRecord, fieldInfo)
                End If
            Next fieldInfo
            g = g.ParentGroup
        Loop
    End Sub

' Fix ManualTotalSummary of parent groups.
#End Region
End Class

3.A Grid Grouping control is setup with options to display the summary cells in caption and enable the optimizations required. Use InvalidateAll option for InsertRemoveBehavior and SortPositionChangedBehavior properties when many records change sort position for a short time. Use ScrollWithImmediateUpdate if ScrollWindow should be called to minimize painting when sort position of limited number of records is changed. GridGroupingControl will be detached from CurrencyManager, and then access the list directly to solely rely on ListChanged events.

this.gridGroupingControl1.UpdateDisplayFrequency = 0; // 0 if manual updates only from timer_tick
this.gridGroupingControl1.UseDefaultsForFasterDrawing = true;
this.gridGroupingControl1.CounterLogic = EngineCounters.YAmount;
this.gridGroupingControl1.AllowedOptimizations = EngineOptimizations.DisableCounters | EngineOptimizations.RecordsAsDisplayElements;
this.gridGroupingControl1.CacheRecordValues = false;
this.gridGroupingControl1.InsertRemoveBehavior = GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate;
this.gridGroupingControl1.SortPositionChangedBehavior = GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate;
this.gridGroupingControl1.BindToCurrencyManager = false;
this.gridGroupingControl1.TableDescriptor.ChildGroupOptions.ShowCaptionSummaryCells = true;
this.gridGroupingControl1.TableDescriptor.ChildGroupOptions.ShowSummaries = true;
this.gridGroupingControl1.TableDescriptor.ChildGroupOptions.CaptionSummaryRow = "Caption";
' 0 if manual updates only from timer_tick
Me.gridGroupingControl1.UpdateDisplayFrequency = 0
Me.gridGroupingControl1.UseDefaultsForFasterDrawing = True
Me.gridGroupingControl1.CounterLogic = EngineCounters.YAmount
Me.gridGroupingControl1.AllowedOptimizations = EngineOptimizations.DisableCounters Or EngineOptimizations.RecordsAsDisplayElements
Me.gridGroupingControl1.CacheRecordValues = False
Me.gridGroupingControl1.InsertRemoveBehavior = GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate
Me.gridGroupingControl1.SortPositionChangedBehavior = GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate
Me.gridGroupingControl1.BindToCurrencyManager = False
Me.gridGroupingControl1.TableDescriptor.ChildGroupOptions.ShowCaptionSummaryCells = True
Me.gridGroupingControl1.TableDescriptor.ChildGroupOptions.ShowSummaries = True
Me.gridGroupingControl1.TableDescriptor.ChildGroupOptions.CaptionSummaryRow = "Caption"

4.Setup ManualTotalSummary for the columns Freight and EmployeeID. The ManualTotalSummary.Total value will be retrieved and displayed in summary or caption cell in QueryCellStyleInfo event handler. It tracks the changes in sort positions of columns Freight and EmployeeID by handling PropertyChanged event.

ManualTotalSummaryTable tbs = (ManualTotalSummaryTable)this.gridGroupingControl1.Table;
tbs.TotalSummaries.Add("Freight");
tbs.TotalSummaries.Add("EmployeeID");
tbs.TableDirty = true; // needed after changing TotalSummaries ...

// Totals with delta support for ListChanged events - replacement for built-in summaries of grouping engine.
this.gridGroupingControl1.QueryCellStyleInfo += new GridTableCellStyleInfoEventHandler(gridGroupingControl1_QueryCellStyleInfo);
private void gridGroupingControl1_QueryCellStyleInfo(object sender, GridTableCellStyleInfoEventArgs e)
{
    Element el = e.TableCellIdentity.DisplayElement;
    ManualTotalSummaryTable table = el.ParentTable as ManualTotalSummaryTable;

    if (table == null)
        return;

    //using (MeasureTime.Measure("gridGroupingControl1_QueryCellStyleInfo"))
    {

        if (Element.IsCaption(el))
        {

            if (e.Style.TableCellIdentity.ColIndex > 3)
            {

                //e.Style.CellValue = e.Style.TableCellIdentity.ColIndex;

                // You need to provide here manually the code to lookup the summaries you want to display here.

                // e.TableCellIdentity.Column and e.TableCellIdentity.SummaryColumn will be null

                // you can get the column as follows:
                GridColumnDescriptor column = this.gridGroupingControl1.TableModel.GetHeaderColumnDescriptorAt(e.TableCellIdentity.ColIndex);

                if (column != null && table.TotalSummaries.IndexOf(column.MappingName) != -1)
                {
                    int index = column.TableDescriptor.Fields.IndexOf(column.FieldDescriptor);
                    IManualTotalSummaryArraySource ts = (el is Syncfusion.Grouping.Group ? el : el.ParentGroup) as IManualTotalSummaryArraySource;
                    ManualTotalSummary manualTotalSummary = ts.GetManualTotalSummaryArray()[index];
                    e.Style.CellValue = manualTotalSummary.Total;
                    e.Style.CellValueType = typeof(double);
                    e.Style.Format = "0.00";
                }

                // Using that column you could try and identify the summary that should be displayed in this cell.
            }
        }

        else if (el is GridSummaryRow)
        {

            // you can get the column as follows:
            GridColumnDescriptor column = this.gridGroupingControl1.TableModel.GetHeaderColumnDescriptorAt(e.TableCellIdentity.ColIndex);

            if (column != null && table.TotalSummaries.IndexOf(column.MappingName) != -1)
            {
                int index = column.TableDescriptor.Fields.IndexOf(column.FieldDescriptor);
                IManualTotalSummaryArraySource ts = (el is Syncfusion.Grouping.Group ? el : el.ParentGroup) as IManualTotalSummaryArraySource;
                ManualTotalSummary manualTotalSummary = ts.GetManualTotalSummaryArray()[index];
                e.Style.CellValue = manualTotalSummary.Total;
                e.Style.CellValueType = typeof(double);
                e.Style.Format = "0.00";
            }

            // Using that column you could try and identify the summary that should be displayed in this cell.
        }
    }
}
Dim tbs As ManualTotalSummaryTable = CType(Me.gridGroupingControl1.Table, ManualTotalSummaryTable)
tbs.TotalSummaries.Add("Freight")
tbs.TotalSummaries.Add("EmployeeID")
tbs.TableDirty = True ' needed after changing TotalSummaries...

' Totals with delta support for ListChanged events - replacement for built-in summaries of grouping engine.
AddHandler gridGroupingControl1.QueryCellStyleInfo, AddressOf gridGroupingControl1_QueryCellStyleInfo

Private Sub gridGroupingControl1_QueryCellStyleInfo(ByVal sender As Object, ByVal e As GridTableCellStyleInfoEventArgs)
Dim el As Element = e.TableCellIdentity.DisplayElement
Dim table As ManualTotalSummaryTable = TryCast(el.ParentTable, ManualTotalSummaryTable)

If table Is Nothing Then
    Return
End If

'using (MeasureTime.Measure("gridGroupingControl1_QueryCellStyleInfo"))

If Element.IsCaption(el) Then

If e.Style.TableCellIdentity.ColIndex > 3 Then

    'e.Style.CellValue = e.Style.TableCellIdentity.ColIndex;

    ' You need to provide here manually the code to lookup the summaries you want to display here.

    ' e.TableCellIdentity.Column and e.TableCellIdentity.SummaryColumn will be null

    ' you can get the column as follows:
    Dim column As GridColumnDescriptor = Me.gridGroupingControl1.TableModel.GetHeaderColumnDescriptorAt(e.TableCellIdentity.ColIndex)

    If column IsNot Nothing AndAlso table.TotalSummaries.IndexOf(column.MappingName) <> -1 Then
        Dim index As Integer = column.TableDescriptor.Fields.IndexOf(column.FieldDescriptor)
        Dim ts As IManualTotalSummaryArraySource = TryCast((If(TypeOf el Is Syncfusion.Grouping.Group, el, el.ParentGroup)), IManualTotalSummaryArraySource)
        Dim manualTotalSummary As ManualTotalSummary = ts.GetManualTotalSummaryArray()(index)
        e.Style.CellValue = manualTotalSummary.Total
        e.Style.CellValueType = GetType(Double)
        e.Style.Format = "0.00"
    End If

    ' Using that column you could try and identify the summary that should be displayed in this cell.
End If

ElseIf TypeOf el Is GridSummaryRow Then

    ' you can get the column as follows:
    Dim column As GridColumnDescriptor = Me.gridGroupingControl1.TableModel.GetHeaderColumnDescriptorAt(e.TableCellIdentity.ColIndex)

    If column IsNot Nothing AndAlso table.TotalSummaries.IndexOf(column.MappingName) <> -1 Then
        Dim index As Integer = column.TableDescriptor.Fields.IndexOf(column.FieldDescriptor)
        Dim ts As IManualTotalSummaryArraySource = TryCast((If(TypeOf el Is Syncfusion.Grouping.Group, el, el.ParentGroup)), IManualTotalSummaryArraySource)
        Dim manualTotalSummary As ManualTotalSummary = ts.GetManualTotalSummaryArray()(index)
        e.Style.CellValue = manualTotalSummary.Total
        e.Style.CellValueType = GetType(Double)
        e.Style.Format = "0.00"
    End If

    ' Using that column you could try and identify the summary that should be displayed in this cell.
End If
End Sub

5.Enable highlighting the cells changed in all the columns.

for (int c = 0; c < gridGroupingControl1.TableDescriptor.Columns.Count; c++)
    this.gridGroupingControl1.TableDescriptor.Columns[c].AllowBlink = true;
this.gridGroupingControl1.BlinkTime = 100;
For c As Integer = 0 To gridGroupingControl1.TableDescriptor.Columns.Count - 1
    Me.gridGroupingControl1.TableDescriptor.Columns(c).AllowBlink = True
Next c
Me.gridGroupingControl1.BlinkTime = 100

6.To optimize performance, grid is updated manually (UpdateDisplayFrequency = 0) at regular intervals. A timer is used to keep track of the duration of time periods. The code to track the changes in Freight and EmployeeID columns and to update the grid rows is written inside the timer_tick event handler where the update is done manually by making a call toUpdate method. Timer interval is set to 100, which means that there would be an update for every 100 ms. This implementation pushes in the pending updates every 100 ms and updates 1000 records each time.

void timer_tick(object sender, EventArgs e)
{
GridTableDescriptor td = this.gridGroupingControl1.TableDescriptor;
ManualTotalSummaryTable tbs = ((ManualTotalSummaryTable)this.gridGroupingControl1.Table);
int i = 0;
using (MeasureTime.Measure("Form1.timer_tick"))
{
int count = 1000;

if (this.gridGroupingControl1.SortPositionChangedBehavior == GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate)
{

if (sortedByFreight || gridGroupingControl1.TestDeleteRecords || gridGroupingControl1.TestInsertRecords || gridGroupingControl1.TestChangeGroup)
        count = 200; // when sort position is changed this is much more demanding, let's do less records then.

if (sortedByEmployeeID)
        count = 50; // each update will cause records being shifted around so let's do even less records. You could also check out InvalidateAll option instead above ...
}

for (i = 0; i < count; i++)
{
    ManualTotalSummaries.DataSet1.OrdersRow dataRow;

    // Insert Records

    if (gridGroupingControl1.TestInsertRecords)
    {

      if (i % 10 == 0)
      {
         dataRow = northwindDataSet1.Orders.NewOrdersRow();
         dataRow.CustomerID = i.ToString() + (j++).ToString();
         dataRow.EmployeeID = i;
         dataRow.Freight = i / 10;
         dataRow.ShipVia = 0;
         dataRow.Table.Rows.Add(dataRow);
         continue;
      }
    }

    if (northwindDataSet1.Orders.Rows.Count == 0)
    {
        this.gridGroupingControl1.Update();
        return;
    }
    int newIndex = random.Next(northwindDataSet1.Orders.Rows.Count);
    dataRow = northwindDataSet1.Orders[newIndex];
    
    // Delete records

    if (gridGroupingControl1.TestDeleteRecords)
    {

        if (i % 12 == 0)
        {
            dataRow.Delete();
            continue;
        }
    }

    // Change records
    
    // Freight
    decimal freight = (decimal)dataRow.Freight + Math.Round((decimal)random.Next(-100, 100) / 10000, 2);
    int employeeID = (int)(random.NextDouble() * 1000);
    dataRow.BeginEdit();
    decimal oldFreight = dataRow.Freight;
    dataRow.Freight = freight;
    dataRow.EmployeeID = employeeID;

    if (gridGroupingControl1.TestChangeGroup)
    {
    
        // Change Group Category
    
        if (i == 10)
        {
            tbs.AddChangedField(new ChangedFieldInfo(td, "ShipVia", dataRow.ShipVia, 0));
            dataRow.ShipVia = 0;
        }
    }

    // fires ListChanged event
    dataRow.EndEdit();
}

// Optionally manually flush changes

if (this.gridGroupingControl1.UpdateDisplayFrequency == 0)
    this.gridGroupingControl1.Update();
}
}
Private Sub timer_tick(ByVal sender As Object, ByVal e As EventArgs)
Dim td As GridTableDescriptor = Me.gridGroupingControl1.TableDescriptor
Dim tbs As ManualTotalSummaryTable = (CType(Me.gridGroupingControl1.Table, ManualTotalSummaryTable))
Dim i As Integer = 0
Using MeasureTime.Measure("Form1.timer_tick")
Dim count As Integer = 1000

If Me.gridGroupingControl1.SortPositionChangedBehavior = GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate Then

    If sortedByFreight OrElse gridGroupingControl1.TestDeleteRecords OrElse gridGroupingControl1.TestInsertRecords OrElse gridGroupingControl1.TestChangeGroup Then
        count = 200 ' when sort position is changed this is much more demanding, let's do less records then.
    End If

    If sortedByEmployeeID Then
                ' each update will cause records being shifted around so let's do even less records. You could also check out InvalidateAll option instead above...
                count = 50
    End If
End If

For i = 0 To count - 1
    Dim dataRow As ManualTotalSummaries.DataSet1.OrdersRow

    ' Insert Records

    If gridGroupingControl1.TestInsertRecords Then

        If i Mod 10 = 0 Then
            dataRow = northwindDataSet1.Orders.NewOrdersRow()
            dataRow.CustomerID = i.ToString() & (j).ToString()
            j += 1
            dataRow.EmployeeID = i
            dataRow.Freight = i \ 10
            dataRow.ShipVia = 0
            dataRow.Table.Rows.Add(dataRow)
            Continue For
        End If
    End If

    If northwindDataSet1.Orders.Rows.Count = 0 Then
        Me.gridGroupingControl1.Update()
        Return
    End If

    Dim newIndex As Integer = random.Next(northwindDataSet1.Orders.Rows.Count)
    dataRow = northwindDataSet1.Orders(newIndex)


    ' Delete records

    If gridGroupingControl1.TestDeleteRecords Then

        If i Mod 12 = 0 Then
            dataRow.Delete()
            Continue For
        End If
    End If

    ' Change records

    ' Freight
    Dim freight As Decimal = CDec(dataRow.Freight) + Math.Round(CDec(random.Next(-100, 100)) / 10000, 2)
    Dim employeeID As Integer = CInt(Fix(random.NextDouble() * 1000))
    dataRow.BeginEdit()
    Dim oldFreight As Decimal = dataRow.Freight
    dataRow.Freight = freight
    dataRow.EmployeeID = employeeID

    If gridGroupingControl1.TestChangeGroup Then

        ' Change Group Category

        If i = 10 Then
            tbs.AddChangedField(New ChangedFieldInfo(td, "ShipVia", dataRow.ShipVia, 0))
            dataRow.ShipVia = 0
        End If
    End If

    ' fires ListChanged event
    dataRow.EndEdit()
Next i

' Optionally manually flush changes

If Me.gridGroupingControl1.UpdateDisplayFrequency = 0 Then
    Me.gridGroupingControl1.Update()
End If
End Using
End Sub

7.It should also take care of UnboundFields whose values are usually dependent on changes to other fields. If unbound fields are used, user can tell the engine the fields that the unbound field is dependent on, by setting theReferencedFields property. When ReferencedFields is set and the engine detects changes to the unbound field, it will then automatically mark the field as changed. This subsequently can affect sort order, group attachment, and so on.

//Adds Unbound field 'ShipVia_CompanyName'.
gridGroupingControl1.TableDescriptor.UnboundFields.Add("ShipVia_CompanyName");
            
//Informs the engine about dependency on ShipVia of this field.
gridGroupingControl1.TableDescriptor.UnboundFields["ShipVia_CompanyName"].ReferencedFields = "ShipVia";
'Adds Unbound field 'ShipVia_CompanyName'.
gridGroupingControl1.TableDescriptor.UnboundFields.Add("ShipVia_CompanyName")

'Informs the engine about dependency on ShipVia of this field.
gridGroupingControl1.TableDescriptor.UnboundFields("ShipVia_CompanyName").ReferencedFields = "ShipVia"

The grid will be looks like the following while updating.
Performance_img4

High Frequency Updates

This section discusses an example that will use to make high frequency updates in an efficient manner. It shows sort position changes, inserting and removing of records from a timer event. At start up, the GridGroupingControl is sorted by Column 1 and changes to fields in that column affects the sort position of a record. The background colors of the cells in records are also dependent on the value in column 1. This dependency is specified with ReferencedFields property. The changes are highlighted for a short period of time after a change is detected.

ReferencedFields property, as the name implies, saves a list of field names referred by a given field. The engine will use these fields in the ListChanged event to determine the cells to be updated when change is made in an underlying field.

ReferencedFields property is very user interactive option and provides options to test the performance of the grid by enabling or disabling grouping, sorting, and filtering in the midst of heavy updates. It also allows you to change the timer frequency that controls the throughput i.e., the number of updates per unit time. At run time, you can also vary the amount of time the changes are highlighted.

Implementation

This example demonstrates the frequent updates that occur in random cells across the grid grouping control, while keeping the CPU usage at minimum level. A timer changes cells in short intervals, inserts and removes rows. When you run the sample you also need to open up the Task Manager to notice the CPU usage while the sample runs. You should be able to start up multiple instances without slowing down your machine.

Sample Location:

<Installed_Location>\Syncfusion\EssentialStudio\<Version_Number>\Windows\Grid.Grouping.Windows\Samples\Performance\Grouping Trader Grid Test Demo

1.Set up GridGroupingControl and load it with some random data. Enable the optimizations as required.

GridGroupingControl gridGroupingControl1 = new GridGroupingControl();
gridGroupingControl1.VerticalThumbTrack = true;
gridGroupingControl1.HorizontalThumbTrack = true;
gridGroupingControl1.TableOptions.VerticalPixelScroll = true;
gridGroupingControl1.DataSource = GetRandomDataTable();
this.gridGroupingControl1.ShowGroupDropArea = true;

//Uses less memory for internal binary tree structures.
gridGroupingControl1.CounterLogic = EngineCounters.YAmount;
gridGroupingControl1.AllowedOptimizations = EngineOptimizations.DisableCounters | EngineOptimizations.RecordsAsDisplayElements;

//Uses faster GDI drawing.
gridGroupingControl1.UseDefaultsForFasterDrawing = true;

//Skips Currency Manager.
gridGroupingControl1.BindToCurrencyManager = false;

//Immediately updates after each ListChanged event.
gridGroupingControl1.UpdateDisplayFrequency = 1;

//Scroll Window will cause immediate update.
gridGroupingControl1.InsertRemoveBehavior = GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate;
gridGroupingControl1.SortPositionChangedBehavior = GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate;

//Insert RemoveBehavior or SortPositionChangedBehavior takes effect only when InvalidateAll is set to false.
gridGroupingControl1.InvalidateAllWhenListChanged = false;
Dim gridGroupingControl1 As New GridGroupingControl()
gridGroupingControl1.VerticalThumbTrack = True
gridGroupingControl1.HorizontalThumbTrack = True
gridGroupingControl1.TableOptions.VerticalPixelScroll = True
gridGroupingControl1.DataSource = GetRandomDataTable()
Me.gridGroupingControl1.ShowGroupDropArea = True

'Uses less memory for internal binary tree structures.
gridGroupingControl1.CounterLogic = EngineCounters.YAmount
gridGroupingControl1.AllowedOptimizations = EngineOptimizations.DisableCounters Or EngineOptimizations.RecordsAsDisplayElements

'Uses faster GDI drawing.
gridGroupingControl1.UseDefaultsForFasterDrawing = True

'Skips Currency Manager.
gridGroupingControl1.BindToCurrencyManager = False

'Immediately updates after each ListChanged event.
gridGroupingControl1.UpdateDisplayFrequency = 1

'Scroll Window will cause immediate update.
gridGroupingControl1.InsertRemoveBehavior = GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate
gridGroupingControl1.SortPositionChangedBehavior = GridListChangedInsertRemoveBehavior.ScrollWithImmediateUpdate

'Insert RemoveBehavior or SortPositionChangedBehavior takes effect only when InvalidateAll is set to false.
gridGroupingControl1.InvalidateAllWhenListChanged = False

2.Set AllowBlink to true for all the columns in order to enable highlighting cells for a short period of time when a change is detected. Engine.AddBaseStylesForBlinking method is used to add base styles for customization of the appearance of blink cells. Initialize base styles for various blink states. PrepareViewStyleInfo is handled to set custom base style for a newly added record. A cell change is highlighted by checking its BlinkState. BlinkState indicates whether the cell’s value is increased or reduced or if the record has been recently added. If its state is either Increased or Reduced, its back color and text colors are changed.

void gridGroupingControl1_TableControlPrepareViewStyleInfo(object sender, GridTableControlPrepareViewStyleInfoEventArgs e)
{
    GridTableCellStyleInfo style = (GridTableCellStyleInfo)e.Inner.Style;
    BlinkState blinkState = gridGroupingControl1.GetBlinkState(style.TableCellIdentity);

    if (blinkState != BlinkState.None)
    {

        if (blinkState == BlinkState.NewRecord)
        {
            e.Inner.Style.BaseStyle = "CustomStyle";
        }
    }
}
Private Sub gridGroupingControl1_TableControlPrepareViewStyleInfo(ByVal sender As Object, ByVal e As GridTableControlPrepareViewStyleInfoEventArgs)
    Dim style As GridTableCellStyleInfo = CType(e.Inner.Style, GridTableCellStyleInfo)
    Dim blinkState As BlinkState = gridGroupingControl1.GetBlinkState(style.TableCellIdentity)

    If blinkState IsNot BlinkState.None Then

        If blinkState Is BlinkState.NewRecord Then
            e.Inner.Style.BaseStyle = "CustomStyle"
        End If
    End If
End Sub

3.A timer event is handled to insert and remove the records. This results in frequent list changes at regular intervals.

bool skipTimer = false;
private void timer_Tick(object sender, EventArgs e)
{

if (skipTimer)
    return;

timerCount++;

try
{

    for (int i = 0; i < m_numberUpdatesPerTick; i++)
    {

        //  Application.DoEvents();

        int recordNumber = rand.Next(table.Rows.Count - 1);
        int rowNumber = recordNumber + 1;
        int col = rand.Next(16) + 1;
        int columnNumber = col + 1;
        DataRow dataRow = table.Rows[recordNumber];

        if (!(dataRow[col] is DBNull))
            dataRow[col] = (int)(Convert.ToDouble(dataRow[col]) * (rand.Next(50) / 100.0f + 0.8));// rand.Next(100);

    }

    // Insert or remove a row

    if (insertRemoveCount == 0)
        return;

    if (toggleInsertRemove > 0 && (timerCount % insertRemoveModulus) == 0)
    {
        iCount = ++iCount % (toggleInsertRemove * 2);
        shouldInsert = iCount < toggleInsertRemove;

        if (shouldInsert)
        {

            for (int r = 0; r < insertRemoveCount; r++)
            {
                int recordNumber = 5;// rand.Next(table.Rows.Count - 1);

                int next = rand.Next(100);
                object[] row = new object[]{"H"+ti.ToString("00000"),next+1,next+2, next+3,next+4,next+5,next+6, next+7,next+8,next+9,next+10, next+11,next+12,next+13,next+14, next+15,next+16,next+17, next+18,next+19,next+20};
                ti++;
                DataRow dataRow = table.NewRow();
                dataRow.ItemArray = row;
                table.Rows.InsertAt(dataRow, recordNumber);

                //table.Rows.Add(dataRow);
            }
        }

        else
        {

            for (int r = 0; r < insertRemoveCount; r++)
            {
                int recordNumber = 5; //rand.Next(m_set.Count - 1);
                int rowNumber = recordNumber + 1;

                // Underlying data structure (this could be a data table or whatever structure

                // you use behind a virtual grid).

                if (table.Rows.Count > 10)
                    table.Rows.RemoveAt(recordNumber);
            }
        }
    }

}
finally
{
}
}
Private skipTimer As Boolean = False

Private Sub timer_Tick(ByVal sender As Object, ByVal e As EventArgs)

    If skipTimer Then
        Return
    End If

    timerCount += 1

    Try

        For i As Integer = 0 To m_numberUpdatesPerTick - 1

            '  Application.DoEvents();

            Dim recordNumber As Integer = rand.Next(table.Rows.Count - 1)
            Dim rowNumber As Integer = recordNumber + 1
            Dim col As Integer = rand.Next(16) + 1
            Dim columnNumber As Integer = col + 1
            Dim dataRow As DataRow = table.Rows(recordNumber)

            If Not(TypeOf dataRow(col) Is DBNull) Then
                dataRow(col) = CInt(Fix(Convert.ToDouble(dataRow(col)) * (rand.Next(50) / 100.0f + 0.8))) ' rand.Next(100);
            End If
        Next i

        ' Insert or remove a row

        If insertRemoveCount = 0 Then
            Return
        End If

        If toggleInsertRemove > 0 AndAlso (timerCount Mod insertRemoveModulus) = 0 Then
            iCount = ++iCount Mod (toggleInsertRemove * 2)
            shouldInsert = iCount < toggleInsertRemove

            If shouldInsert Then

                For r As Integer = 0 To insertRemoveCount - 1
                    Dim recordNumber As Integer = 5 ' rand.Next(table.Rows.Count - 1);
                    Dim [next] As Integer = rand.Next(100)
                    Dim row() As Object = {"H" & ti.ToString("00000"),[next]+1,[next]+2, [next]+3,[next]+4,[next]+5,[next]+6, [next]+7,[next]+8,[next]+9,[next]+10, [next]+11,[next]+12,[next]+13,[next]+14, [next]+15,[next]+16,[next]+17, [next]+18,[next]+19,[next]+20}
                    ti += 1
                    Dim dataRow As DataRow = table.NewRow()
                    dataRow.ItemArray = row
                    table.Rows.InsertAt(dataRow, recordNumber)

                    'table.Rows.Add(dataRow);
                Next r

            Else

                For r As Integer = 0 To insertRemoveCount - 1
                    Dim recordNumber As Integer = 5 'rand.Next(m_set.Count - 1);
                    Dim rowNumber As Integer = recordNumber + 1

                    ' Underlying data structure (this could be a data table or whatever structure

                    ' you use behind a virtual grid).

                    If table.Rows.Count > 10 Then
                        table.Rows.RemoveAt(recordNumber)
                    End If
                Next r
            End If
        End If

    Finally
    End Try
End Sub

4.QueryCellStyleInfo is handled to enable coloring of the cells. The background colors of the cells in the records are dependent on the column 1 values. This dependency is specified using Referenced Fields property. To make it user friendly, you can use CheckBox control to enable or disable this coloring. Hook this event if the checked state of the CheckBox is true; unhook otherwise.

Color[] colors = new Color[] {          
Color.FromArgb( 0x85, 0xbf, 0x75 ),
Color.FromArgb( 0xb4, 0xe7, 0xf2 ),
Color.FromArgb( 0xff, 0xbf, 0x34 ),
Color.FromArgb( 0x82, 0x2e, 0x1b ),
Color.FromArgb( 0x3a, 0x86, 0x7e )};    

void gridGroupingControl1_QueryCellStyleInfo(object sender, GridTableCellStyleInfoEventArgs e)
{
    GridTableCellStyleInfo style = (GridTableCellStyleInfo)e.Style;

    if (e.TableCellIdentity.TableCellType == GridTableCellType.RecordFieldCell
        || e.TableCellIdentity.TableCellType == GridTableCellType.AlternateRecordFieldCell)
    {

        if (e.TableCellIdentity.Column.FieldDescriptor.FieldPropertyType == typeof(string))
            return;
        {

            // Get the value from column "1" and color all cells in record based

            // on this value.
            Record r = e.Style.TableCellIdentity.DisplayElement.GetRecord();
            object value = r.GetValue("1");
            int v = Convert.ToInt32(value) % colors.Length;
            e.Style.BackColor = colors[v];
        }
    }
}

private void checkBoxColor_CheckedChanged(object sender, System.EventArgs e)
{
    isUIChanged = true;

    GridTableDescriptor td = this.gridGroupingControl1.TableDescriptor;
    try
    {

        if (this.checkBoxColor.Checked)
        {

            // Callback for dynamically coloring cells
            gridGroupingControl1.QueryCellStyleInfo += new GridTableCellStyleInfoEventHandler(gridGroupingControl1_QueryCellStyleInfo);

            // The color of these cells depends on value of cell "1". If engines ListChanged handler

            // detects a change to column "1" it should also automatically repaint the dependent columns
            for (int i = 2; i <= 20; i++)
                gridGroupingControl1.TableDescriptor.Fields[i.ToString()].ReferencedFields = "1";

        }

        else
        {
            gridGroupingControl1.QueryCellStyleInfo -= new GridTableCellStyleInfoEventHandler(gridGroupingControl1_QueryCellStyleInfo);
            gridGroupingControl1.TableDescriptor.Fields.LoadDefault();
        }
        this.gridGroupingControl1.Refresh();
    }

    catch (Exception ex)
    {
        Trace.WriteLine(ex.ToString());
    }
    finally
    {
        isUIChanged = false;
    }
}
Private colors() As Color = { Color.FromArgb(&H85, &Hbf, &H75), Color.FromArgb(&Hb4, &He7, &Hf2), Color.FromArgb(&Hff, &Hbf, &H34), Color.FromArgb(&H82, &H2e, &H1b), Color.FromArgb(&H3a, &H86, &H7e)}

Private Sub gridGroupingControl1_QueryCellStyleInfo(ByVal sender As Object, ByVal e As GridTableCellStyleInfoEventArgs)
    Dim style As GridTableCellStyleInfo = CType(e.Style, GridTableCellStyleInfo)

    If e.TableCellIdentity.TableCellType = GridTableCellType.RecordFieldCell OrElse e.TableCellIdentity.TableCellType = GridTableCellType.AlternateRecordFieldCell Then

        If e.TableCellIdentity.Column.FieldDescriptor.FieldPropertyType Is GetType(String) Then
            Return
        End If

            ' Get the value from column "1" and color all cells in record based

            ' on this value.
            Dim r As Record = e.Style.TableCellIdentity.DisplayElement.GetRecord()
            Dim value As Object = r.GetValue("1")
            Dim v As Integer = Convert.ToInt32(value) Mod colors.Length
            e.Style.BackColor = colors(v)
    End If
End Sub

Private Sub checkBoxColor_CheckedChanged(ByVal sender As Object, ByVal e As System.EventArgs)
    isUIChanged = True

    Dim td As GridTableDescriptor = Me.gridGroupingControl1.TableDescriptor
    Try

        If Me.checkBoxColor.Checked Then
            ' Callback for dynamically coloring cells
            AddHandler gridGroupingControl1.QueryCellStyleInfo, AddressOf gridGroupingControl1_QueryCellStyleInfo

            ' The color of these cells depends on value of cell "1". If engines ListChanged handler

            ' detects a change to column "1" it should also automatically repaint the dependent columns

            For i As Integer = 2 To 20
                gridGroupingControl1.TableDescriptor.Fields(i.ToString()).ReferencedFields = "1"
            Next i

        Else
            RemoveHandler gridGroupingControl1.QueryCellStyleInfo, AddressOf gridGroupingControl1_QueryCellStyleInfo
            gridGroupingControl1.TableDescriptor.Fields.LoadDefault()
        End If
        Me.gridGroupingControl1.Refresh()

    Catch ex As Exception
        Trace.WriteLine(ex.ToString())
    Finally
        isUIChanged = False
    End Try
End Sub

5.Add three more CheckBoxes to include options to enable or disable Grouping, Sorting, and Filtering at runtime. Handle their checked changed events to add the code for adding and removing groups, sorted columns and record filters. For example, if the checked state of the check box is true then group the table against a column. When its checked state is turned to false, simply ungroup the table.

//Sorts Option.

private void checkBoxSorting_CheckedChanged(object sender, System.EventArgs e)
{

    if (this.checkBoxSorting.Checked)
    {
        gridGroupingControl1.TableDescriptor.SortedColumns.Clear();
        gridGroupingControl1.TableDescriptor.SortedColumns.Add("1");
        gridGroupingControl1.TableDescriptor.SortedColumns.Add("2");
    }

    else
    {
        gridGroupingControl1.TableDescriptor.SortedColumns.Clear();
    }
    this.gridGroupingControl1.Refresh();
}

//Groups Option.

private void checkBoxGrouping_CheckedChanged(object sender, System.EventArgs e)
{

    if (this.checkBoxGrouping.Checked)
    {
        gridGroupingControl1.TableDescriptor.GroupedColumns.Clear();
        gridGroupingControl1.TableDescriptor.GroupedColumns.Add("1");
        this.gridGroupingControl1.Table.ExpandAllGroups();
    }

    else
    {
        gridGroupingControl1.TableDescriptor.GroupedColumns.Clear();
    }
    this.gridGroupingControl1.Refresh();
}

// Filters Option.

private void checkBoxFilter_CheckedChanged(object sender, System.EventArgs e)
{

    if (this.checkBoxFilter.Checked)
    {
        gridGroupingControl1.TableDescriptor.RecordFilters.Clear();

        //Gets the filter expression from a Text Box.
        gridGroupingControl1.TableDescriptor.RecordFilters.Add(this.textBoxFilter.Text);
    }

    else
    {
        gridGroupingControl1.TableDescriptor.RecordFilters.Clear();
    }
    this.gridGroupingControl1.Refresh();
}
'Sorts Option.

Private Sub checkBoxSorting_CheckedChanged(ByVal sender As Object, ByVal e As System.EventArgs)

    If Me.checkBoxSorting.Checked Then
        gridGroupingControl1.TableDescriptor.SortedColumns.Clear()
        gridGroupingControl1.TableDescriptor.SortedColumns.Add("1")
        gridGroupingControl1.TableDescriptor.SortedColumns.Add("2")

    Else
        gridGroupingControl1.TableDescriptor.SortedColumns.Clear()
    End If
    Me.gridGroupingControl1.Refresh()
End Sub

'Groups Option.
Private Sub checkBoxGrouping_CheckedChanged(ByVal sender As Object, ByVal e As System.EventArgs)

    If Me.checkBoxGrouping.Checked Then
        gridGroupingControl1.TableDescriptor.GroupedColumns.Clear()
        gridGroupingControl1.TableDescriptor.GroupedColumns.Add("1")
        Me.gridGroupingControl1.Table.ExpandAllGroups()

    Else
        gridGroupingControl1.TableDescriptor.GroupedColumns.Clear()
    End If
    Me.gridGroupingControl1.Refresh()
End Sub

' Filters Option.

Private Sub checkBoxFilter_CheckedChanged(ByVal sender As Object, ByVal e As System.EventArgs)

    If Me.checkBoxFilter.Checked Then
        gridGroupingControl1.TableDescriptor.RecordFilters.Clear()

        'Gets the filter expression from a Text Box.
        gridGroupingControl1.TableDescriptor.RecordFilters.Add(Me.textBoxFilter.Text)

    Else
        gridGroupingControl1.TableDescriptor.RecordFilters.Clear()
    End If
    Me.gridGroupingControl1.Refresh()
End Sub

6.Two TrackBar controls are used to change the frequencies of the Timer and BlinkTime. The frequencies that are set by the end user are integrated into the grid grouping control in their respective TrackBarScroll event handlers.

//Changes the Blink Time Frequency.

private void trackBarBlinkFrequency_Scroll(object sender, System.EventArgs e)
{
    this.gridGroupingControl1.BlinkTime = this.trackBarBlinkFrequency.Value * 100;

    if (this.gridGroupingControl1.BlinkTime == 0)
        this.labelBlinkTime.Text = String.Format("Disabled.");

    else
        this.labelBlinkTime.Text = String.Format("{0} milliseconds.", gridGroupingControl1.BlinkTime);
    this.gridGroupingControl1.Refresh();
}

private void trackBarTimer_Scroll(object sender, System.EventArgs e)
{

    if (this.trackBarTimer.Value == 0)
    {
        timer.Enabled = false;
        this.labelTimerInterval.Text = String.Format("Timer disabled.");
    }

    else
    {
        timer.Interval = 1000 / (this.trackBarTimer.Value * trackBarTimer.Value);
        timer.Enabled = true;
        this.labelTimerInterval.Text = String.Format("Every {0} milliseconds.", timer.Interval);
    }
}
'Changes the Blink Time Frequency.

Private Sub trackBarBlinkFrequency_Scroll(ByVal sender As Object, ByVal e As System.EventArgs)
    Me.gridGroupingControl1.BlinkTime = Me.trackBarBlinkFrequency.Value * 100

    If Me.gridGroupingControl1.BlinkTime = 0 Then
        Me.labelBlinkTime.Text = String.Format("Disabled.")

    Else
        Me.labelBlinkTime.Text = String.Format("{0} milliseconds.", gridGroupingControl1.BlinkTime)
    End If
    Me.gridGroupingControl1.Refresh()
End Sub

Private Sub trackBarTimer_Scroll(ByVal sender As Object, ByVal e As System.EventArgs)

    If Me.trackBarTimer.Value = 0 Then
        timer.Enabled = False
        Me.labelTimerInterval.Text = String.Format("Timer disabled.")

    Else
        timer.Interval = 1000 / (Me.trackBarTimer.Value * trackBarTimer.Value)
        timer.Enabled = True
        Me.labelTimerInterval.Text = String.Format("Every {0} milliseconds.", timer.Interval)
    End If
End Sub

Given below is a sample screen shot. While running the sample, apply grouping, sorting and filtering, and then check for the CPU time usage in TaskManager to detect grid performance.

Performance_img5

Grouping Performance

This section focuses on a sample that lets you check the performance of the grid grouping control by toggling various options that can affect the speed of the grid. The different options include Sort and Categorize the records, calculating maximum column width, custom sorting and multi-threading (in case if a multiprocessor system is available).

Implementation

The grouping performance is implemented in the following sample

Sample Location:

<Installed_Location>\Syncfusion\EssentialStudio\<Version_Number>\Windows\Grid.Grouping.Windows\Samples\Performance\Grouping Performance Demo

The following is the list of the options used.

Sort and Categorize - This option will enable grouping and sorting by assigning a group and sort order.

UseDataViewSort - It uses the class GroupingSortList to wrap DataView with IBindingList. It also implements IGroupingList interface. This allows sorting the data view directly instead of relying on grouping engine to sort.

CalculateMaximumColumnWidth - When enabled, the maximum number of characters found in record field cells is calculated for columns. This will be used in re sizing the columns to optimal width. Affects TableDescriptor.AllowCalculateMaxColumnWidth property.

MultiThreading - When set to true, this option will allow multithreading. It allows you to calculate the count in a separate thread when all records are categorized. Affects Table.AllowThreading property. Enable this only on true multiprocessor machines otherwise systems calculating counts in separate thread will slow categorization down.

ListChanging Options - It also includes options to insert, remove and modify the records in data source. All the changes will be immediately updated manually by making a call to Update method.

UseScrollWindow - When enabled, inserting and removing cells will be optimized by scrolling window contents and only invalidating new cells. If set to false, it results in repainting the whole display. Affects TableControl.OptimizeInsertRemoveCells property.

ExpandAll or CollapseAll - By using these options, you can track the time taken to expand or collapse all the groups and memory usage too.

After enabling the options required, click the LoadGrid button. This will then check for the options requested and apply those options before painting the grid. After loading, it also displays a log to print various performance measures like time taken to paint the grid, physical memory usage, etc. The log will continue to display these performance measure at every instant the grid options are changed.

IList Grouping Performance

The IList bound to GridGroupingControl has been implemented with an optimization process for grouping columns to improve performance. Grouping a column that has IList bound reduces the time taken to refresh the control after grouping. The grouping performance will improve with huge data loaded

Set OptimizeIListGroupingPerformance to true to enable grouping optimization over the IList data source. The following code illustrates how to enable grouping optimization.

Enable Real Time Updates

The OptimizeIListGroupingPerformance method has to be called to enable real time updates with data source from GridGroupingControl.

this.gridGroupingControl1.SourceListListChanged += new TableListChangedEventHandler(gridGroupingControl1_SourceListListChanged);

void gridGroupingControl1_SourceListListChanged(object sender, TableListChangedEventArgs e)
{
    // Enable RealTime Updates
    this.gridGroupingControl1.OptimizeIListGroupingPerformance(sender, e);
}
AddHandler gridGroupingControl1.SourceListListChanged, AddressOf gridGroupingControl1_SourceListListChanged

Private Sub gridGroupingControl1_SourceListListChanged(ByVal sender As Object, ByVal e As TableListChangedEventArgs)
    ' Enable RealTime Updates
    Me.gridGroupingControl1.OptimizeIListGroupingPerformance(sender, e)
End Sub