From 231710ca68be3974ad227e0a5254ff341f8b7448 Mon Sep 17 00:00:00 2001
From: F K <54195004+fredjk-gh@users.noreply.github.com>
Date: Tue, 30 Jan 2024 20:49:53 -0500
Subject: [PATCH] [Core + Framework] Performance improvements for re-drawing
 the grid (#141)

This proposes a *new method* on IObservatoryCore:  `SetGridItems(worker, items)` which does 2 things:
* Clears the grid
* Adds the given items.

Effectively replaces the entire content of the grid -- which is something a handful of plugins now do.

Why add this when you could just call Core's Clear + AddGridItems methods?? So it can all be done within the same rendering suppression "scope" to reduce flickering and increase rendering speed.  Speaking of rendering suppression, I have implemented such rendering suppression "scope" which uses Listview's built-in Begin/EndUpdate() in combination with temporary removal of the sort comparer (as is done for read-alls).  This was also applied to the existing AddGridItems(worker, items) method as well, addressing a TODO.
---
 .../PluginManagement/PluginCore.cs            | 54 ++++++++++++++++++-
 ObservatoryCore/UI/PluginHelper.cs            |  1 +
 ObservatoryCore/UI/PluginListView.cs          | 26 +++++++--
 ObservatoryFramework/Interfaces.cs            |  7 +++
 ObservatoryFramework/ObservatoryFramework.xml |  7 +++
 5 files changed, 89 insertions(+), 6 deletions(-)

diff --git a/ObservatoryCore/PluginManagement/PluginCore.cs b/ObservatoryCore/PluginManagement/PluginCore.cs
index 5e919f0..d351d07 100644
--- a/ObservatoryCore/PluginManagement/PluginCore.cs
+++ b/ObservatoryCore/PluginManagement/PluginCore.cs
@@ -2,6 +2,7 @@
 using Observatory.Framework.Files;
 using Observatory.Framework.Interfaces;
 using Observatory.NativeNotification;
+using Observatory.UI;
 using Observatory.Utils;
 using System;
 using System.Collections.ObjectModel;
@@ -114,11 +115,27 @@ namespace Observatory.PluginManagement
 
         public void AddGridItems(IObservatoryWorker worker, IEnumerable<object> items)
         {
-            //TODO: Use better bulk handling here.
+            BeginBulkUpdate(worker);
+
             foreach (var item in items)
             {
                 worker.PluginUI.DataGrid.Add(item);
             }
+
+            EndBulkUpdate(worker);
+        }
+
+        public void SetGridItems(IObservatoryWorker worker, IEnumerable<object> items)
+        {
+            BeginBulkUpdate(worker);
+
+            worker.PluginUI.DataGrid.Clear();
+            foreach (var item in items)
+            {
+                worker.PluginUI.DataGrid.Add(item);
+            }
+
+            EndBulkUpdate(worker);
         }
 
         public void ClearGrid(IObservatoryWorker worker, object templateItem)
@@ -186,5 +203,40 @@ namespace Observatory.PluginManagement
         {
             NativePopup.CloseAll();
         }
+
+        private void BeginBulkUpdate(IObservatoryWorker worker)
+        {
+            PluginListView? listView = FindPluginListView(worker);
+            if (listView == null) return;
+
+            ExecuteOnUIThread(() => { listView.SuspendDrawing(); });
+        }
+
+        private void EndBulkUpdate(IObservatoryWorker worker)
+        {
+            PluginListView? listView = FindPluginListView(worker);
+            if (listView == null) return;
+
+            ExecuteOnUIThread(() => { listView.ResumeDrawing(); });
+        }
+
+        private PluginListView? FindPluginListView(IObservatoryWorker worker)
+        {
+            if (worker.PluginUI.PluginUIType != PluginUI.UIType.Basic
+                || !(worker.PluginUI.UI is Panel)) return null;
+
+            PluginListView? listView = null;
+            Panel panel = worker.PluginUI.UI as Panel;
+
+            foreach (var control in panel.Controls)
+            {
+                if (control?.GetType() == typeof(PluginListView))
+                {
+                    listView = (PluginListView)control;
+                    return listView;
+                }
+            }
+            return null;
+        }
     }
 }
diff --git a/ObservatoryCore/UI/PluginHelper.cs b/ObservatoryCore/UI/PluginHelper.cs
index d9fb8fa..899dcd6 100644
--- a/ObservatoryCore/UI/PluginHelper.cs
+++ b/ObservatoryCore/UI/PluginHelper.cs
@@ -61,6 +61,7 @@ namespace Observatory.UI
             {
                 Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom | AnchorStyles.Top
             };
+            plugin.PluginUI.UI = panel;
 
             IObservatoryComparer columnSorter;
             if (plugin.ColumnSorter != null)
diff --git a/ObservatoryCore/UI/PluginListView.cs b/ObservatoryCore/UI/PluginListView.cs
index 3897123..469978e 100644
--- a/ObservatoryCore/UI/PluginListView.cs
+++ b/ObservatoryCore/UI/PluginListView.cs
@@ -1,20 +1,17 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Security.Permissions;
 using System.Text;
 using System.Threading.Tasks;
+using System.Windows.Forms;
 
 namespace Observatory.UI
 {
     internal class PluginListView : ListView
     {
-        [DllImport("user32.dll")]
-        private static extern int SendMessage(IntPtr hWnd, int wMsg, bool wParam, int lParam);
-        
-        private const int WM_SETREDRAW = 11;
-
         public PluginListView()
         {
             OwnerDraw = true;
@@ -28,6 +25,25 @@ namespace Observatory.UI
             base.GridLines = false;//We should prevent the default drawing of gridlines.
         }
 
+        // Stash for performance when doing large UI updates.
+        private IComparer? comparer = null;
+
+        public void SuspendDrawing()
+        {
+            BeginUpdate();
+            comparer = ListViewItemSorter;
+        }
+
+        public void ResumeDrawing()
+        {
+            if (comparer != null)
+            {
+                ListViewItemSorter = comparer;
+                comparer = null;
+            }
+            EndUpdate();
+        }
+
         private static void DrawBorder(Graphics graphics, Pen pen, Rectangle bounds, bool header = false)
         {
             
diff --git a/ObservatoryFramework/Interfaces.cs b/ObservatoryFramework/Interfaces.cs
index e595679..7fd6541 100644
--- a/ObservatoryFramework/Interfaces.cs
+++ b/ObservatoryFramework/Interfaces.cs
@@ -187,6 +187,13 @@ namespace Observatory.Framework.Interfaces
         /// <param name="items">Grid items to be added. Object types should match original template item used to create the grid.</param>
         public void AddGridItems(IObservatoryWorker worker, IEnumerable<object> items);
 
+        /// <summary>
+        /// Replace the contents of the grid with the provided items.
+        /// </summary>
+        /// <param name="worker">Reference to the calling plugin's worker interface.</param>
+        /// <param name="items">Grid items to be added. Object types should match original template item used to create the grid.</param>
+        public void SetGridItems(IObservatoryWorker worker, IEnumerable<object> items);
+
         /// <summary>
         /// Clears basic UI grid, removing all items.
         /// </summary>
diff --git a/ObservatoryFramework/ObservatoryFramework.xml b/ObservatoryFramework/ObservatoryFramework.xml
index d6172cd..bd14ee8 100644
--- a/ObservatoryFramework/ObservatoryFramework.xml
+++ b/ObservatoryFramework/ObservatoryFramework.xml
@@ -1605,6 +1605,13 @@
             <param name="worker">Reference to the calling plugin's worker interface.</param>
             <param name="items">Grid items to be added. Object types should match original template item used to create the grid.</param>
         </member>
+        <member name="M:Observatory.Framework.Interfaces.IObservatoryCore.SetGridItems(Observatory.Framework.Interfaces.IObservatoryWorker,System.Collections.Generic.IEnumerable{System.Object})">
+            <summary>
+            Replace the contents of the grid with the provided items.
+            </summary>
+            <param name="worker">Reference to the calling plugin's worker interface.</param>
+            <param name="items">Grid items to be added. Object types should match original template item used to create the grid.</param>
+        </member>
         <member name="M:Observatory.Framework.Interfaces.IObservatoryCore.ClearGrid(Observatory.Framework.Interfaces.IObservatoryWorker,System.Object)">
             <summary>
             Clears basic UI grid, removing all items.