Keeping UI Responsive with foreach loop in WinForms

953 Views Asked by At

I have created a Winforms App that uses the Open Hardware Monitor to display PC temps in a gauge format using Live Charts. I am aware that the following code causes the UI to become unresponsive as the charts are updated but I am not able to figure out how to implement any form of threading to work with this or how to change how I have coded the application to keep the UI responsive. The timer will tick every two seconds which fetches the values and updates the gauges.

References added to the project are:

  • OpenHardwareMonitorLib
  • LiveCharts.Winforms + Dependencies

See my code below, any advice would be appreciated.

UserControls.TempGauge

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Media;

namespace PCDashboard.UserControls
{
public partial class TempGauge : UserControl
{
    public TempGauge(string name)
    {
        try
        {
            //Initialize
            InitializeComponent();

            //Gauge Properties
            this.Name = name;
            gaugeName.Text = name;
            gauge.Uses360Mode = true;
            gauge.From = 0;
            gauge.To = 110;
            gauge.Value = 0;
            gauge.HighFontSize = 60;
            gauge.Base.Foreground = System.Windows.Media.Brushes.White;
            gauge.InnerRadius = 0;
            gauge.GaugeBackground = Classes.Colours.Blue;
        }
        catch (Exception)
        {

        }
    }
  }
}

MainForm

using OpenHardwareMonitor.Hardware;
using PCDashboard.Classes;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace PCDashboard.Forms
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private void MainForm_Load(object sender, EventArgs e)
        {
            try
            {
                GetSystemTemps(false);
                timer.Enabled = true;
            }
            catch (Exception)
            {
            }
        }

        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void GetSystemTemps(bool UpdateGauge)
        {
            try
            {
                UpdateVisitor updateVisitor = new UpdateVisitor();
                Computer computer = new Computer();
                computer.Open();

                //Which components to read?
                computer.CPUEnabled = true;
                computer.GPUEnabled = true;
                computer.MainboardEnabled = true;
                computer.RAMEnabled = true;

                computer.Accept(updateVisitor);

                foreach (IHardware hardware in computer.Hardware)
                {
                    foreach (ISensor sensor in hardware.Sensors.Where(
                        x => x.SensorType == SensorType.Temperature))
                    {
                        if (UpdateGauge)
                        {
                            UserControls.TempGauge tempGauge =
                                ((UserControls.TempGauge)gaugeContainer
                                    .Controls[sensor.Name]);

                            if (tempGauge != null)
                            {
                                tempGauge.gauge.Value = Math.Round(
                                    double.Parse(sensor.Value.ToString()), 0);
                            }
                        }
                        else
                        {
                            UserControls.TempGauge tempGauge =
                                new UserControls.TempGauge(sensor.Name);
                            gaugeContainer.Controls.Add(tempGauge);
                        }
                    }
                }

                computer.Close();
            }
            catch (Exception)
            {
            }
        }

        private void timer_Tick(object sender, EventArgs e)
        {
            try
            {
                GetSystemTemps(true);
            }
            catch (Exception)
            {
            }
        }
    }
}

enter image description here

2

There are 2 best solutions below

3
Theodor Zoulias On BEST ANSWER

My suggestion is to separate the data retrieval from the presentation. This way you will be able to offload the heavy work of data retrieval to a background thread, without having to worry how you will update the UI controls at the same time. Here is how you could make a GetSensorData method that fetches the data as an array of ValueTuple<string, double> elements, representing the name and the value of each sensor:

private (string, double)[] GetSensorData()
{
    var list = new List<(string, double)>();
    Computer computer = new Computer();
    computer.Open();
    //...
    foreach (IHardware hardware in computer.Hardware)
    {
        foreach (ISensor sensor in hardware.Sensors.Where(
            x => x.SensorType == SensorType.Temperature))
        {
            list.Add(sensor.Name, double.Parse(sensor.Value.ToString()));
        }
    }
    computer.Close();
    return list.ToArray();
}

Then you could use the Task.Run method to offload the heavy work of retrieving the data to a background thread (a ThreadPool thread). This method returns a Task, that could be awaited asynchronously so that the code below the await has available data to work with.

private async Task UpdateSystemTempsAsync(bool updateGauge)
{
    var data = await Task.Run(() => GetSensorData()); // Offload to thread-pool

    // While awaiting the UI remains responsive

    // After the await we are back in the UI thread

    foreach (var (sensorName, sensorValue) in data)
    {
        if (updateGauge)
        {
            UserControls.TempGauge tempGauge =
                ((UserControls.TempGauge)gaugeContainer.Controls[sensorName]);
            if (tempGauge != null)
            {
                tempGauge.gauge.Value = Math.Round(sensorValue, 0);
            }
        }
        else
        {
            var tempGauge = new UserControls.TempGauge(sensorName);
            gaugeContainer.Controls.Add(tempGauge);
        }
    }
}

Finally you'll have to make the event handlers async:

private async void MainForm_Load(object sender, EventArgs e)
{
    try
    {
        await UpdateSystemTempsAsync(false);
        timer.Enabled = true;
    }
    catch { }
}

private async void timer_Tick(object sender, EventArgs e)
{
    timer.Enabled = false;
    try
    {
        await UpdateSystemTempsAsync(true);
        timer.Enabled = true;
    }
    catch { }
}
1
Joel Coehoorn On

Using a timer for this is perfectly reasonable. But there are three things you can do to help your app stay responsive.

First, I'd check the Interval property on the timer. Anything less than 100 is probably too small, and you can probably go quite a bit higher. Even 250 is still 4 times per second, and 400 more than twice per second.

Second, I'd bracket the loop with calls to SuspendLayout() and ResumeLayout(). This will help your form be more efficient about redrawing in a batch.

Finally, IIRC you can set the timer's Enabled property to false for form Move events, and back to true again for ResizeEnd events, to disable updates while someone is moving the form around. This is from way back for me, though, so you'll want to test this one.