Ericrs
Ericrs

Reputation: 29

c# Updating Text Box with Powershell Console details in real time

So I'm new to C#. I had been building all my tools (7 years worth of tools) in Powershell as I could never obtain a Visual Studio License but now that I have it...

I've been reading about system.componentmodel.backgroundworker however I'm having trouble implementing it.

Heres my code without Background worker and with. Can someone help me in seeing how I can implement it so my UI Doesn't go unresponsive when I kick off the Powershell jobs and I can actually get (somewhat) real updates in the text box

using System;
using System.Text;
using System.Windows.Forms;
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.IO;


namespace ServerStatusChecks
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }


    private void button1_Click(object sender, EventArgs e)
        {

            // run our script and put the result into our textbox
            // NOTE: make sure to change the path to the correct location of your script
            textBox1.Text = RunScript(LoadScript(@"c:\utils\Script.ps1"));
        }

        // helper method that takes your script path, loads up the script
        // into a variable, and passes the variable to the RunScript method
        // that will then execute the contents
        private string LoadScript(string filename)
        {

            try
            {
                // Create an instance of StreamReader to read from our file.
                // The using statement also closes the StreamReader.
                using (StreamReader sr = new StreamReader(filename))
                {


                    // use a string builder to get all our lines from the file
                    StringBuilder fileContents = new StringBuilder();

                    // string to hold the current line
                    string curLine;

                    // loop through our file and read each line into our
                    // stringbuilder as we go along
                    while ((curLine = sr.ReadLine()) != null)
                    {
                        // read each line and MAKE SURE YOU ADD BACK THE
                        // LINEFEED THAT IT THE ReadLine() METHOD STRIPS OFF
                        fileContents.Append(curLine + "\n");
                    }

                    // call RunScript and pass in our file contents
                    // converted to a string
                    return fileContents.ToString();
                }
            }
            catch (Exception e)
            {
                // Let the user know what went wrong.
                string errorText = "The file could not be read:";
                errorText += e.Message + "\n";
                return errorText;
            }



        }


        // Takes script text as input and runs it, then converts
        // the results to a string to return to the user
        private string RunScript(string scriptText)
        {
            // create Powershell runspace
            Runspace runspace = RunspaceFactory.CreateRunspace();

            // open it
            runspace.Open();

            // create a pipeline and feed it the script text
            Pipeline pipeline = runspace.CreatePipeline();
            pipeline.Commands.AddScript(scriptText);

            // add an extra command to transform the script output objects into nicely formatted strings
            // remove this line to get the actual objects that the script returns. For example, the script
            // "Get-Process" returns a collection of System.Diagnostics.Process instances.
            pipeline.Commands.Add("Out-String");

            // execute the script
            Collection<PSObject> results = pipeline.Invoke();

            // close the runspace
            pipeline.Dispose();
            runspace.Close();

            // convert the script result into a single string
            StringBuilder stringBuilder = new StringBuilder();
            foreach (PSObject obj in results)
            {
                stringBuilder.AppendLine(obj.ToString());

            }

            // return the results of the script that has
            // now been converted to text
            return stringBuilder.ToString();
        }
        private void Form1_Load(object sender, EventArgs e)
        {

        }
        private void textBox1_TextChanged(object sender, EventArgs e)
        {


        }


    }

}

Now here's the code with me trying to implement BW (It's a bit of a monster as I've tried to get it to work several ways and it's a mess now)

using System;
using System.Text;
using System.Windows.Forms;
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.ComponentModel;
using System.Threading;
using System.IO;


namespace ServerStatusChecks
{
    public partial class Form1 : Form
    {
        private BackgroundWorker bw = new BackgroundWorker();
        public Form1()
        {
            InitializeComponent();
            bw.WorkerReportsProgress = true;
            bw.WorkerSupportsCancellation = true;
            bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        }


        private string LoadScript(string filename)
        {

            try
            {
                // Create an instance of StreamReader to read from our file.
                // The using statement also closes the StreamReader.
                using (StreamReader sr = new StreamReader(filename))
                {


                    // use a string builder to get all our lines from the file
                    StringBuilder fileContents = new StringBuilder();

                    // string to hold the current line
                    string curLine;

                    // loop through our file and read each line into our
                    // stringbuilder as we go along
                    while ((curLine = sr.ReadLine()) != null)
                    {
                        // read each line and MAKE SURE YOU ADD BACK THE
                        // LINEFEED THAT IT THE ReadLine() METHOD STRIPS OFF
                        fileContents.Append(curLine + "\n");
                    }

                    // call RunScript and pass in our file contents
                    // converted to a string
                    return fileContents.ToString();
                }
            }
            catch (Exception e)
            {
                // Let the user know what went wrong.
                string errorText = "The file could not be read:";
                errorText += e.Message + "\n";
                return errorText;

            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (bw.IsBusy != true)
            {
                bw.RunWorkerAsync();
            }
            // run our script and put the result into our textbox
            // NOTE: make sure to change the path to the correct location of your script
            textBox1.Text = RunScript(LoadScript(@"c:\utils\Script.ps1"));
        }
        private void bw_DoWork(object sender, EventArgs e)
        {

        }

        // Takes script text as input and runs it, then converts
        // the results to a string to return to the user
        private string RunScript(string scriptText)
        {
            // create Powershell runspace
            Runspace runspace = RunspaceFactory.CreateRunspace();

            // open it
            runspace.Open();

            // create a pipeline and feed it the script text
            Pipeline pipeline = runspace.CreatePipeline();
            pipeline.Commands.AddScript(scriptText);

            // add an extra command to transform the script output objects into nicely formatted strings
            // remove this line to get the actual objects that the script returns. For example, the script
            // "Get-Process" returns a collection of System.Diagnostics.Process instances.
            pipeline.Commands.Add("Out-String");

            // execute the script
            Collection<PSObject> results = pipeline.Invoke();

            // close the runspace
            pipeline.Dispose();
            runspace.Close();

            // convert the script result into a single string
            StringBuilder stringBuilder = new StringBuilder();
            foreach (PSObject obj in results)
            {
                stringBuilder.AppendLine(obj.ToString());

            }

            // return the results of the script that has
            // now been converted to text
            return stringBuilder.ToString();
        }

        // helper method that takes your script path, loads up the script
        // into a variable, and passes the variable to the RunScript method
        // that will then execute the contents
        private void Form1_Load(object sender, EventArgs e)
        {
            Thread.Sleep(1000);
        }
        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            Thread.Sleep(1000);
        }

    }

}

Upvotes: 0

Views: 1344

Answers (1)

grorkster
grorkster

Reputation: 42

The problem is that you're not running anything in the bw_DoWork event, which is where the actual background work is run. You should change this section of your code to the following:

 private void button1_Click(object sender, EventArgs e)
    {
        if (bw.IsBusy != true)
        {
            bw.RunWorkerAsync();
        }
    }
    private void bw_DoWork(object sender, EventArgs e)
    {
        // run our script and put the result into our textbox
        // NOTE: make sure to change the path to the correct location of your script
        textBox1.Text = RunScript(LoadScript(@"c:\utils\Script.ps1"));
    }

The reason this works is that the bw_DoWork method is an event listener, which means it "listens" for when the RunWorkerAsync() method is called, which you do when the button is clicked. bw_DoWork knows to listen for the RunWorkerAsync() function is because the line you put at the top of the program:

bw.DoWork += new DoWorkEventHandler(bw_DoWork);

This basically tells DoWork to sit and wait for the event to fire off. This is commonly referred to as "subscribing to the event". When it does, a new thread spawns that does whatever is in bw's DoWork event while your original thread, which contains the UI (and is commonly refered to as the 'UI Thread') keeps on trucking. Events and Asynchronous threads are a tricky subject to wrap your head around, but once you do it's a fantastic piece of .NET.

EDIT: Alright, so the above is correct as far as getting bw to run properly, but it causes this error:

Error: {"Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on."}

Above I said that the main thread is called the "UI Thread". This also means that the UI is locked to that thread. Therefore, your BackgroundWorker cannot touch the UI elements. Therefore the offending line is textBox1.Text = RunScript(LoadScript(@"c:\utils\Script.ps1"));

The easiest way to fix this is to have RunScript() pass its value to a public variable, then assign the variable after bw has done its processing. We do this by subscribing to another built-in event in BackgroundWorker called RunWorkerCompleted.

First, declare a new static string at the top of the class:

static string RunScriptResult;

Then, add a new listener to bw by modifying Form1:

public Form1()
    {
        //...
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.RunWorkerCompleted += new bw_RunWorkerCompleted; //new listener event
    }

Next, change bw_DoWork to assign the output to the string we created earlier:

private void bw_DoWork(object sender, EventArgs e)
{
    RunScriptResult = RunScript(LoadScript(@"c:\utils\Script.ps1"));
}

Next, add a new event handler method for RunWorkerCompleted:

private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        textBox1.Text = RunScriptResult;
    }

The above event does not fire until bw is finished, and it runs in the UI thread. These modifications should get you where you need to be.

Upvotes: 1

Related Questions