Reputation: 29
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
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