Search code examples
c#multithreadingvisual-studiowinformsopenhardwaremonitor

Keeping UI Responsive with foreach loop in WinForms


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


Solution

  • 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 { }
    }