I have a timer, that triggers a timed event that iterates through a TreeView
and pings all the IP addresses associated with each node.
I want this to be done in the background on a separate thread. I (probably wrongly) thought that an OnTimedEvent exists on a separate thread to the UI.
However, when my program starts, it is responsive. Then as soon as the timed event triggers the UI becomes unresponsive, doesn't crash but doesn't regain responsiveness until the program is stopped in the debugger and ran again.
I've been messing around with async
and await
in order to get the required behaviour however I am unable to achieve this.
Here's how I initialise the Timer currently.
public Form1()
{
InitializeComponent();
StartTimer();
}
private async void StartTimer()
{
aTimer = new System.Timers.Timer(15000);
await Task.Run(() => aTimer.Elapsed = OnTimedEvent);
aTimer.AutoReset = true;
aTimer.Enabled = true;
aTimer.SynchronizingObject = this;
}
And the OnEventTimed method
private async void OnTimedEvent(Object source, ElapsedEventArgs e)
{
// Iterate through all root nodes
foreach (TreeNode tn in mainTree.Nodes)
{
TreeNodeCollection rootNodes = ((TreeNode)tn).Nodes;
foreach (TreeNode node in rootNodes)
{
// Create ping object
System.Net.NetworkInformation.Ping pinger = new();
PingReply pingReply;
if (node.Tag != null)
{
pingReply = pinger.Send(node.Tag.ToString());
if (pingReply.Status.ToString().Contains("Success"))
{
UpdateUI(node, 1); // If successful, set the image index to show green
}
else if (pingReply.Status.ToString().Contains("Failed"))
{
UpdateUI(node, 2);
}
}
}
// Iterate through all the children of the 'root' nodes
foreach (TreeNode child in tn.Nodes)
{
// Extract all nodes from these children
TreeNodeCollection myNodes = ((TreeNode)child).Nodes;
// Create ping object
System.Net.NetworkInformation.Ping pinger = new();
PingReply pingReply;
// Iterate through each of the nodes, send a ping request and then update the UI based on the result of the ping
foreach (TreeNode node in myNodes)
{
if (node.Tag != null)
{
pingReply = pinger.Send(node.Tag.ToString());
if (pingReply.Status.ToString().Contains("Success"))
{
UpdateUI(node, 1); // If successful, set the image index to show green
}
else if (pingReply.Status.ToString().Contains("Failed"))
{
UpdateUI(node, 2);
}
}
}
}
}
}
I am trying to use await Task.Run(() => aTimer.Elapsed = OnTimedEvent);
but it's not giving me the behaviour I expect any help would be appreciated
CodePudding user response:
My suggestion is to ditch the mischievous System.Timers.Timer
, and use instead a normal System.Windows.Forms.Timer
. Then in the Tick
event handler just offload to the ThreadPool
any work that you don't want to happen on the UI thread. All interaction with UI controls should stay on the UI thread. Here is an (untested) example:
private async void timer_Tick(object sender, EventArgs e)
{
timer.Enabled = false;
//...
if (node.Tag != null)
{
string ip = node.Tag.ToString();
PingReply pingReply = await Task.Run(() => pinger.Send(ip));
//...
timer.Enabled = true;
}
The Task.Run
is the method that does the offloading. The action of the Task.Run
is invoked on the ThreadPool
. The result of the invocation is marshaled back on the UI thread, by await
ing the resulting Task<PingReply>
.
Notice how the Task.Run
delegate is not touching at the slightest the TreeNode
UI elements. It gets an argument of type string
, and returns a result of PingReply
. All the UI-related work is performed exclusively on the UI thread.