- Optimization Properties
- Enabling Required Optimizations
- Reduce Flickering
- Bypassing CurrencyManager
- Caching the Record Values
- Maintaining the Counters
- Inserting and Removing Records Behavior
- Invalidating Layout on Record Changing
- Raising ListChanged event only on Engine
- Update on Sort Position Changed
- Update Frequency on particular time
- Using Default Graphics for faster Drawing
- Using Old ListChanged event
- Blinking Time
- Memory Performance – Engine Optimizations
- Record ListChanged Performance
- High Frequency Updates
- Grouping Performance
- IList Grouping Performance
Contact Support
Performance in Windows Forms GridGrouping control
21 Jan 202524 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
.
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 andIndexOf
operations. When initializing table, the engine will check if criteria forDisableCounters
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.
-
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
-
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
-
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.
-
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
-
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
-
Handle the
ButtonClick
event in order to populate the grid when the button is clicked. It also callsLogMemoryUsage
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
-
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
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
-
ManualTotalSummary
class makes use ofManualTotalSummaryTable
class, which derivesGridTable
to calculate the new total.ManualTotalSummaryTable
class overridesOnRecordChanged
event in order to track record changes and keeps track of old and new values of theChangedField
. For each entry inManualTotalSummaryTable.TotalSummaries
, aManualTotalSummary
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
- A Grid Grouping control is setup with options to display the summary cells in caption and enable the optimizations required. Use
InvalidateAll
option forInsertRemoveBehavior
andSortPositionChangedBehavior
properties when many records change sort position for a short time. UseScrollWithImmediateUpdate
ifScrollWindow
should be called to minimize painting when sort position of limited number of records is changed. GridGroupingControl will be detached fromCurrencyManager
, and then access the list directly to solely rely onListChanged
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"
- 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
- 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
- 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 thetimer_tick
event handler where the update is done manually by making a call toUpdate method. Timer interval is set to100
, which means that there would be an update for every100 ms
. This implementation pushes in the pending updates every100 ms
and updates1000 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
- 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. WhenReferencedFields
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.
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
- 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
- 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
- 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
-
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
- 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
- Two
TrackBar
controls are used to change the frequencies of the Timer andBlinkTime
. The frequencies that are set by the end user are integrated into the grid grouping control in their respectiveTrackBarScroll
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.
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