Home > Software engineering >  C# use multithread to write to richtextBox
C# use multithread to write to richtextBox

Time:05-13

I am writing a C# script to do a ping test on a network. I have it mostly working, except the part to write to richtextbox. I need to write results as the ping test is happening. I know it needs multithreading, but I have spent the past week trying to figure it out, and cannot. Here is the code I have, minus the unnecessary parts. The part I am having trouble with is the RunPingTest function. I will also include my sample xml for the IP addresses. All the buttons and other functions otherwise work as intended.

using System.Diagnostics;          
using System.Windows;              
using System.Xml.Linq;             
using System.Collections.Generic;  
using System.Net.NetworkInformation;
using System;
using System.Threading;

namespace NetTestingTool
{
   public partial class NetTest : Window
   {
      static string dataFolder = "D:";
      static readonly string configPath = dataFolder   "\\ConfigFiles";
      
      //get HostIPAddresses.xml
      XElement GroupNames = XElement.Load(configPath   "\\HostIPAddresses.xml");
      bool testIsRunning = false;
      //Start the initial GUI

      public NetTest()
      {
         InitializeComponent();
         //populate the dropdown
         foreach (var groupElement in GroupNames.Elements("Group"))
         {
            if (groupElement.Attribute("name").Value != null)
            {
               dropDown.Items.Add(groupElement.Attribute("name").Value);
            }
         }
      }

      //********** Button Actions **********
      //run the network test
      public void BtnStart_Click(object sender, RoutedEventArgs e)
      {
         //clear richtextbox first
         resultsBox.Document.Blocks.Clear();
         //first, make sure the user has selected a group
         if (dropDown.SelectedItem != null)
         {
            string HostSelection = (string)dropDown.SelectedItem;
            //create a list of IP addresses based on group chosen
            List<string> pingList = new List<string>();
            foreach (var groupElement in GroupNames.Elements("Group"))
            {
               //resultsBox.AppendText(groupElement.Attribute("name").Value);
               if (groupElement.Attribute("name").Value == HostSelection)
               {
                  // resultsBox.AppendText(groupElement);
                  foreach (var hostSystem in groupElement.Elements("HostSystem"))
                  {
                     if (hostSystem.Attribute("IPAddress").Value != null)
                     {
                        pingList.Add(hostSystem.Attribute("IPAddress").Value);
                     }
                  }
               }
            }
            
            /*Should this be outside BtnStart_Click?
            currently cannot because it uses pingList*/
            void RunPingTest(object? obj)
            {
               //do the ping test
               resultsBox.AppendText("Doing the ping test \r"); //Just kidding, it isn't
               foreach (var pingElement in pingList)
               {
                  //this needs to be able to write to resultsBox as the ping happens (richtextbox)
                   Ping myPing = new Ping();
                   resultsBox.AppendText("Pinging "   pingElement   ". . . ");
                   PingReply reply = myPing.Send(pingElement, 2000); //ip and timeout
                   string results = reply.Status.ToString();
                   resultsBox.AppendText(results   "\r");
               }
               resultsBox.AppendText("\r\n Finished ping testing");
            }
         }
      } 
        //other buttons here
   }
}

xml for IPAddresses:

<?xml version="1.0"?>
<Systems>
   <Group name="Group1">
      <HostSystem name="System1" IPAddress="1.1.1.1"> </HostSystem> 
      <HostSystem name="System2" IPAddress="2.2.2.2"> </HostSystem>
      <HostSystem name="System3" IPAddress="3.3.3.3"> </HostSystem>  
   </Group>
   <Group name="Group2">
      <HostSystem name="System4" IPAddress="4.4.4.4"> </HostSystem>
      <HostSystem name="System5" IPAddress="5.5.5.5"> </HostSystem>
      <HostSystem name="System6" IPAddress="6.6.6.6"> </HostSystem>  
   </Group>

CodePudding user response:

Well, in the code you posted you never call the method RunPingTest which is probably why your RichTextBox never gets updated.

However, if you want to do this asynchronously, the Ping class contains a method called SendPingAsync with a number of overloads that returns a Task<PingReply> and allows you to just use async/await. No need to introduce any kind of Thread or BackgroundWorker or manually deal with the Dispatcher.

private async void BtnStart_Click(object sender, RoutedEventArgs e)
{
    var pingList = new List<string>
    {
       "1.1.1.1",
       "2.2.2.2",
       "3.3.3.3",
       "4.4.4.4"
    };

    resultsBox.Document.Blocks.Clear();
    resultsBox.AppendText("Doing the ping test\r");
    using (var ping = new Ping())
    {
        foreach (var ip in pingList)
        {
            resultsBox.AppendText($"Pinging {ip} . . . ");
            PingReply reply = await ping.SendPingAsync(ip, 2000);
            resultsBox.AppendText($"{reply.Status} \r");
        }
    }
}

The important thing is to make your event handler async void and to await the calls to SendPingAsync. You might also want to consider disabling 'BtnStart' until the method completes, otherwise users will be able to click on it while a ping test is still running.

CodePudding user response:

You can't access controls from threads other than your GUI thread.

A quick fix is to dispatch your AppendText to the main thread by saving the main Dispatcher object in your constructor and using it to invoke your append (using BeginInvoke) on the main thread.

Among other things you should probably work on improving are not instantiating multiple Ping objects and not using a rich text box at all, what you have are log entry lines which are much better modeled as an observable array of strings bound to an ItemsControl.

Edit: Also use Path.Combine to combine paths, your string concatenation is incredibly brittle.

CodePudding user response:

I have enough comments about your design and code, but to help you with your question here how to update resultsBox from a thread:

if (resultsBox.InvokeRequired)
{               
  this.BeginInvoke((MethodInvoker)delegate()
  {
     // you can do anything to resultBox here  (1)
  });
}
else
{
   // do it here (2)
}

To avoid duplicate code at 1 and 2 create a new method UpdateResultBox() to put all your logic, your code will be:

if (resultsBox.InvokeRequired)
{               
  this.BeginInvoke((MethodInvoker)delegate()
  {
     UpdateResultsBox();
  });
}
else
{
     UpdateResultsBox();
}

private void UpdateResultsBox()
{
  // put your logic here
}
  • Related