C# WinForms Calling PowerShell 5.1
Problems to Overcome:
- To run PowerShell version 5.1 in C#, I believe you need your C# program to be based on .NET Framework, probably 4.x. This may not be true, but I've already spent far more time on this than I ever expected, so not going to investigate alternatives.
- C# used to offer through Nuget an easy method for adding PowerShell 5.1 to your project. This is no longer true and you now have to take extra steps to obtain a version of System.Management.Automation.dll that is designed for PowerShell 5.1.
- While you can, with extra steps, obtains System.Management.Automation.dll from NuGet, it is not as new as the version used by PowerShell 5.1 on a fully updated Windows 10 system.
- The version of
Get-Service
provided by versions of PowerShell 7.x, and I believe 6.x as well, do NOT provide the ComputerName
parameter, nor any built in method for accessing remote computers. There are options for executing Get-Service
on a remote system and returning the results, but this too can be problematic - as in firewall issues, certain service(s) not running, I'm thinking certain registry key setting can cause issues, etc...
- The
Forms.Timer
class can update controls on a form due to the fact that it uses the same thread, BUT, this means that waiting 20 seconds for Get-Service
to return info from a remote computer will lock that thread and freeze the form.
Building VS C# project with correct version of System.Management.Automation.dll:
Some generalized steps:
Start Visual Studio 2022:
"Create a new project"
C# -> Windows -> Desktop
Windows Forms App (.NET Framework) -> Next
Set "Project name" to project's name
Set "Location" to project's path
Set "Framework" to ".NET Framework 4.8"
"Create"
Working in the Visual Studio 2022 Project:
Tools -> NuGet Package Manager -> Package Manager Console
Enter command to add System.Management.Automation.dll
Get command from here: https://www.nuget.org/packages/System.Management.Automation.dll/
NuGet command: NuGet\Install-Package System.Management.Automation.dll -Version 10.0.10586
Optional:
In PowerShell 5.1
Navigate to the project's \packages\System.Management.Automation.dll.10.0.10586.0\lib\net40
Execute: Copy ([PSObject].Assembly.Location)
The trick to copy System.Management.Automation.dll from Windows' PowerShell 5.1 came from here (Other useful info on the same page):
https://stackoverflow.com/a/13485939/4190564
When designing the form and controls, I believe this is all of the unique settings that I used:
Form:
(Name) = runPowerShellForm
Size = 700, 200
Text = "Test Form for Running PowerShell"
{Set Load to run RunPS}
Label:
(Name) = outputLabel
AutoSize = false
Anchor = Top, Bottom, Left, Right
BackColor = White
Font = Lucida Console, 12pt
ForeColor = Navy
Size = 660, 114
Text = ""
TextBox:
(Name) = computerNameTextBox
Anchor = Bottom, Left, Right
Size = 532, 20
Button:
(Name) = updateComputerButton
Anchor = Bottom, Right
Size = 122, 23
Text = "Update Computer"
{Set button click}
And this is the actual code:
public partial class runPowerShellForm : Form {
private static string _computerName = ".";
private static int _tickCount = 0;
private static System.Timers.Timer _timer = new System.Timers.Timer();
private static Label _outputLabel = null;
private static PowerShell ps = PowerShell.Create();
private static void NewGetService(string computerName) {
_computerName = computerName;
ps = PowerShell.Create();
ps.AddCommand("Get-Service").AddParameter("ComputerName", computerName).AddParameter("Name", "WinDefend");
}
private static void RunPS() {
string LabelText = "Computer: " + _computerName + "\r\nTick Count: " + (++_tickCount).ToString() + "\r\n\r\n";
LabelText += "Status Name DisplayName\r\n";
LabelText += "------ ---- -----------\r\n";
foreach(PSObject result in ps.Invoke()) {
LabelText += String.Format(
"{0,-9}{1,-19}{2}",
result.Members["Status"].Value,
result.Members["Name"].Value,
result.Members["DisplayName"].Value);
}
_outputLabel.BeginInvoke(new UpdateLabel(UpdateMethod), _outputLabel, LabelText);
}
public delegate void UpdateLabel(Label arg1, string arg2);
public static void UpdateMethod(Label labelCtrl, string textStr) {
labelCtrl.Text = textStr;
}
private static void OnTickEvent(Object source, System.Timers.ElapsedEventArgs e) {
RunPS();
}
public runPowerShellForm() {
InitializeComponent();
}
private void updateComputerButton_Click(object sender, EventArgs e) {
NewGetService(this.computerNameTextBox.Text);
this.computerNameTextBox.Text = "";
RunPS();
}
private void runPowerShellForm_Load(object sender, EventArgs e) {
_outputLabel = this.outputLabel;
_timer.Elapsed += OnTickEvent;
_timer.Interval = 30000;
_timer.Enabled = true;
NewGetService(_computerName);
RunPS();
}
private void runPowerShellForm_SizeChanged(object sender, EventArgs e) {
this.computerNameTextBox.Text = this.Size.ToString();
}
}
The Original Answer:
This falls far short of answering the question, but it does illustrate how to call PowerShell from the C# that is called from PowerShell.
If you are trying to execute a single PowerShell command, then maybe the answers to Run PSCmdLets in C# code (Citrix XenDesktop) and How to execute a powershell script using c# and setting execution policy? would do what you want.
I'm doing everything in PowerShell right now, so just easier for me to create a PowerShell script that calls C# that Calls PowerShell. This PowerShell script calls the Run
method of the C# class DoPS
that invokes Get-Service -ComputerName "." -Name "WinDefend"
and then uses WriteLine statements to mimic the expected output of Get-Service
:
Add-Type -Language 'CSharp' -TypeDefinition @'
using System;
using System.Management.Automation;
public class DoPS {
public void Run(){
//Get-Service -ComputerName "." -Name "WinDefend"
PowerShell ps = PowerShell.Create();
ps.AddCommand("Get-Service").AddParameter("ComputerName", ".").AddParameter("Name", "WinDefend");
Console.WriteLine("Status Name DisplayName");
Console.WriteLine("------ ---- -----------");
foreach (PSObject result in ps.Invoke()) {
Console.WriteLine(
"{0,-9}{1,-19}{2}",
result.Members["Status"].Value,
result.Members["Name"].Value,
result.Members["DisplayName"].Value);
}
}
}
'@
$DoPS = [DoPS]::new()
$DoPS.Run()
Which outputs this text:
Status Name DisplayName
------ ---- -----------
Running WinDefend Microsoft Defender Antivirus Service