In the following series of blog posts I will introduce you to C#, Visual Studio and the ConfigMgr SDK, and show you how to produce your own custom tooling easily.
The motivation behind this series of postings is to enable you to create community tools or bespoke tooling to assist you in your day-to-day ConfigMgr role, and thus to enhance the Community overall, as hopefully you’ll produce the very next best tool and we’ll all benefit from it.
To underpin the guide I’ve written a tool called MonitorMP which will keep an eye on the health of your Management Points outside of the ConfigMgr Console, the source code for this tool will be built up and completed by the time we’ve finished with the series of posts, at which point we’ll make the tool made available in both compiled and source code form and everyone that read this guide will feel somehow connected to it :)
Previous postings for this guide
Guide to creating your own ConfigMgr tools – Part 1
Guide to creating your own ConfigMgr tools – Part 2
Guide to creating your own ConfigMgr tools – Part 2 – Extended
In this post we’re finally going to build the MonitorMP tool!
Let’s first lay out our requirements:
- .Net 4.0 as we want this to be highly available, and not require the latest .Net (4.5.1 or 4.5.2) to be installed
- Check all Management Points associated with a Site Server, to see if they respond to HTTP requests, green light, red light visual indicator
- Repeat the test on an interval
- Test HTTP only, HTTPS requires extra handling and is a great idea for a V2 made by the Community
That’s about it, all we want to do is check the Management Points for a response, and maybe schedule a repeating check just to stretch the project out a bit, and to include threading examples for you.
To accomplish this, we’re going to need some tools from the .Net library:
- HttpWebResponse Class
HttpWebResponse allows us to easily open a TCP\IP session to a destination device, issue some HTTP and retrieve the response
- Background Worker Thread Class
A Background Worker thread will allow us to set a schedule for repeating the test, and allows us to interact with the Form\UI thread to update our interface. The great thing about the Background Worker threads are their event support, such as DoWork, RunworkerCompleted, and the most important for us, ProgressChanged. These events can interact with the UI thread allowing us to update the UI with data
You now have two choices, if you are pretty sturdy with Visual Studio and C# already, then download the Source Code here and run the project to see the end result, skipping all the building up steps, or join me as I build the project step-by-step, so that you write it and gain from the experience.
Let’s get underway and step through building out our project together.
- Open Visual Studio and create a new Project
- Select Windows Forms Application
- Give the project the name ManageMP, and sort out the Location (accept the default or choose your development folder if you have one) then Select OK
We’ll begin designing the Form before we lay down a single line of code, so let’s get on with that now.
I’ll be asking you to drag some objects from the Toolbox onto the form, tweaking their properties and position\size attributes.
- Modify the Forms properties
- Set the Size to 667, 348
- Set the Maximum size to 667, 1000 (this sets the maximum form dimensions, 667 width meaning it cannot be adjusted widthways, with 1000 set for the height which lets the user resize lengthways)
- Set the Minimum size to 667,348 (this is the minimum form dimensions, 667 width and 348 height)
- Set the Text to MonitorMP
- You can set the Icon for the Form but this isn’t necessary to progress, you can download one I created earlier from here. Change the Forms Icon, and also change the Default Icon in the projects Properties. I suggest storing the ICO file in the Project folder:
- Select Icon to browse for your ICO file:
- Right click your Project and from the Application tab browse for an ICO file, you can also click Assembly Information to add metadata to the EXE that is shown when you right click it:
- Add a DataGridView
- Drag a DataGridView onto the form
- Set its Name to dgv_Mp
- Set its Location to 13, 12
- Set its Size to 626, 228
- Set the following properties to False
- TabStop
- AllowUserToAddRows
- AllowUserToDeleteRows
- AllowUserToResizeRows
- MultiSelect
- RowHeadersVisible
- ShowEditingIcon
- Set the following properties to True
- ReadOnly
- StandardTab
- Set AutoSizeRowsMode to AllCells
- Set Anchor to Top, Bottom, Left, Right (this allows the DataGridView to grow as you resize the form, we only need to do Top, Bottom as we are not allowing resizing of the form Widthways)
- Set AlternatingRowsDefaultCellStyle to DataGridViewCellStyle { BackColor=Color [A=255, R=224, G=224, B=224] } (Use the ellipses and select BackColor to pick a background colour, light grey, or a colour that you like)
- Right click this DataGridView control, select Edit Columns
- Select Add
- For Name enter c_mpName
- For HeaderText enter Name
- Select Add then Close
- For AutoSizeMode select AllCells
- Select Add
- For Name enter c_siteCode
- For HeaderText enter SiteCode
- Select Add then Close
- For AutoSizeMode select AllCells
- Select Add
- For Name enter c_State
- For ColumnType select DataGridViewImageColumn
- For HeaderText enter State
- Select Add then Close
- For AutoSizeMode select AllCells
- Select Add
- For Name enter c_mpStatus
- For HeaderText enter Status
- Select Add then Close_
- For AutoSizeMode select Fill
- Select OK
- Add a Label
- Drag a Label onto the form
- Set the Name to l_writtenBy
- Set the Text to “Written by X” and replace X with your name!
- Set the Location to 12, 254
- Set Anchor to Bottom
- Set TabIndex to 0
- Add a TextBox
- Drag a TextBox onto the form
- Set the Name to tb_Server
- Set the Location to 164, 254
- Set the Size to 169, 20
- Set Anchor to Bottom
- Set TabIndex to 1
- Add a Checkbox
- Drag a Checkbox onto the form
- Set the Name to cb_Timer
- Set the Location to 339, 254
- Set the Text to Enable Timer
- Set Anchor to Bottom
- Set TabIndex to 3
- Add a NumericUpDown
- Drag a NumericUpDown onto the form
- Set the Name to nud_timerMinutes
- Set the Location to 433, 252
- Set the Size to 47, 20
- Set Anchor to Bottom
- Set the Value to 5
- Set TabIndex to 4
- Add a Button
- Drag a Button onto the form
- Set the Name to b_Go
- Set the Text to Check Management Points
- Set the Location to 486, 250
- Set the Size to 153, 23
- Set Anchor to Bottom
- Set TabIndex to 2
- Add a Status Strip
- Drag a StatusStrip onto the Form
- Set the Name to ss_Messaging
- Right click the StatusStrip and Select Edit Items
- Select StatusLabel and Click Add
- Set the Name for toolStripStatusLabel1 to ssl_Entry
- Set the Text to blank (nothing) otherwise it will look like this:
- Now that is the form laid out, on your end it should look like this with <Name> replaced with your name
- In terms of position and sizing of the forms objects, not a little like this, but actually like this
- I simply compiled the completed project to get the Form showing for the above screenshot, but you should also be able to compile and run it right now to see the same.
Let’s write a line or two of code.
If you are new to coding in C# you’re about to see several cool techniques that help me code solid applications, for the more handy with C# there are no surprises here for you. I’m a mid-tier C# coder I guess, and could do things more efficiently in some places, make more use of .Net, but overall I get there.
Things we’ll cover:
- Methods used by the dompCheck BackgroundWorker thread, so as to populate the DataGridView
- Custom Class Collections to contain collections of custom classes that we’ll use to store multiple properties, and pass around between methods
- Threading, and thread management through global variables, as well as examples of passing our Custom Classes around using the ProgressChanged and RunWorkerCompleted BackgroundWorker thread events
Ok I was really just teasing you, no coding yet, first let’s cover off why I'm making references to the UI Thread, and mention creating a BackgroundWorker thread:
All Windows Form Applications start out life as Single-threaded applications. This means all the code you write for your application, and the User interface controls you add are all being processed by a single thread, called the Foreground thread.
So, if you burn out that thread the UI will lag out and become unresponsive, and if it does it for long enough the Operating System will sense this and offer to kill off the process for us. We’ve all see this at one point in time. Not good. For Console based applications this isn’t much of an issue, unless you need concurrent activities taking place.
To go multi-threaded we hit an immediate wall, a custom thread cannot speak directly to the Foreground threads forms, such as the DataGridView and StatusStrip which we want to manipulate. We can code stuff into a normal Thread from the Thread Class, but it is a work of pain. To overcome this, we use a special kind of thread, and do away with coding our own way out of the situation. We use the BackgroundWorker thread, which is derived from the Thread Class itself, as a place to run our code, and hosts a bunch of methods and events we can fall back on to speak to the Foreground threads Form controls. The key event for reporting progress back up to the UI thread is the ProgressChanged event, which we can fire at will, the other of note is for when our BackgroundWorker thread is stopped, and is called the RunWorkerCompleted event. These two events can interact with the UI thread, allowing us to play with those form objects while still running the custom thread, or coming out of it.
There are a few good reasons for running code on the UI thread, but ideally if you can lob it off to a custom thread to get on with, is much better, things become more fluid in the UI, as in the user experiences a smoother ride. To read more on the BackgroundWorker thread, visit the MSDN library here.
The gap between single-threaded applications and multi-threaded is narrowed further for you, multi-threading your code is now within your reach!
Now let’s really begin coding. I’ll offer up code-blocks for you to copy\paste in, but please do watch out for the browser changing characters such as quotation marks.
Firstly, we need to add some references to the .Net 4.0 classes we want to use in the project
- Double click on the form to be taken to the Code view
- Replace all using clauses with the following:
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Net;
using System.IO;
using System.Management;
using System.Threading;
It should look like this:
- Return to the Form view
We’re going to need some triggers to control the BackgroundWorker threads we’ll create soon, we set these as public.
Add the following code below the Form1_Load method:
public volatile bool mpcheckRunning = false;
public volatile bool mpcheckStop = false;
public volatile bool timerRunning = false;
public volatile bool timerStop = false;
It should look like this:
We set these to volatile as we’re going to access these from threads, volatile forces the compiler to not optimise them, which would result in possibly offering us an indexed value rather than the actual value (think of lazy values). Since we’re checking them from inside a thread we need them to be reliable, and must not change them from multiple threads. Booleans are not such a problem but changing a global string value for example, from multiple threads, could lead to the string becoming corrupted.
Now let’s create two very special classes that we’ll use to pass information around between methods. We’re using an Object Orientated Language, so instead of passing a single property back and forth between methods, or an array of properties like old school style, we can pass an entire object containing several properties, or even a collection of these objects.
We’ll do this when we check the Management Points, we’ll pass the MP Name, MP Port and MP State around as an ‘object’, and we’ll put all of these objects into a Collection, and in turn pass that around. It may sound complicated to begin with, but over time you’ll have to grown into doing this, so as to overcome certain obstacles when it comes to how much information you want to push around between the methods, especially BackgroundWorker thread events.
We need several more variables so let’s create them now.
- Add the following below the previous variables:
public volatile bool mpcheckRunning = false;
public volatile bool mpcheckStop = false;public volatile bool timerRunning = false;
public volatile bool timerStop = false;public volatile int nudtimerMinutes = 5; // Set to 5 to reflect the nud_timerMinutes controls default setting
public volatile ManagementPointCollection globalmpList = new ManagementPointCollection();
private static string unhealthyIcon = "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";
private static string healthyIcon = "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";
It should look like this (the icon strings are longer than this screenshot can show):
We’ve declared a variable representing how often a scheduled check of the Management Points should happen (in minutes) as nudtimerMinutes, we defined a Collection that is used to store the results of a scan for Management Points, and two strings that contain Base64 encoded representations of an Icon for Health and Unhealthy.
We’ll add in a Class called ManagementPoint, and we’ll define some internal properties that we can change such as Name, Port, State and SiteCode.
- Add the following code below the code variables you created previously:
public class ManagementPoint // ManagementPoint Class
{
private string _Name;
private int _Port;
private string _State;
private string _SiteCode;public string Name
{
get { return _Name; }
set { _Name = value; }
}public int Port
{
get { return _Port; }
set { _Port = value; }
}public string State
{
get { return _State; }
set { _State = value; }
}public string SiteCode
{
get { return _SiteCode; }
set { _SiteCode = value; }
}
}
It should look like this:
Now we’ve defined the ManagementPoint Class, let’s define a ManagementPoint Collection Class used as a container for multiple ManagementPoint Classes. This is a really neat way of storing a bunch of ManagementPoint objects and allows us to pass them around the project when needed.
- Add the following code below the ManagementPoint class that you created previously:
public class ManagementPointCollection : System.Collections.CollectionBase // ManagementPoint Collection Class
{
public void Add(ManagementPoint amanagementPoint)
{
List.Add(amanagementPoint);
}public void Remove(int index)
{
if (index > Count - 1 || index < 0)
{}
else
{
List.RemoveAt(index);
}
}public ManagementPoint Item(int Index)
{
return (ManagementPoint)List[Index];
}
}
It should look like this:
This allows us to store MP classes in a Collection, pass them around and handle the Collection using a foreach statement. You’ll also notice that the Collection has three methods called Add, Remove and one called Item to return an object from the Collection based on its Index, this is how we handle the Collection when we put it to use.
Now that is in place, let’s create the basics just to get the thread started, and include the ability to stop it.
- Add a BackgroundWorker thread that will scan the Management Points
- Drag a BackgroundWorker onto the form
- Set the Name to dompCheck
- Set WorkerReportsProgress to True
- Set WorkerSupportsCancellation to True
- Add a BackgroundWorker thread that will schedule a scan if it is enabled
- Drag a BackgroundWorker onto the form
- Set the Name to doScheduling
- Set WorkerReportsProgress to True
- Set WorkerSupportsCancellation to True
- Select the dompCheck BackgroundWorker, you’ll find it has appeared here:
- Now select the Events tab on the Properties pane:
- The three events that the BackgroundWorker thread supports are shown here:
- DoWork handles the actual workload the thread is supposed to carry out
- ProgressChanged can be invoked by us, and it is executed on the UI thread so we get access to the forms controls
- RunWorkerCompleted is called when we exit the thread, it also executes on the UI thread and provides access to the forms controls
- Double click DoWork
- This will take you to the Code view, and will create a new method for DoWork
- Go back to the Form view and repeat this for ProgressChanged and RunWorkerCompleted. This is a very handy way to create the event methods
- Now go find the doScheduling BackgroundWorker using the form view, and repeat the same way that you did with dompCheck and create the three event classes
All three events are now mapped to individual methods for both BackgroundWorker threads, all we need to do now is invoke the threads in our code when we want them.
Let’s create a basic method that I’m using to start the BackgroundWorker thread dompCheck.
Add the following code below the ManagementPointCollection class that you created previously:
private void beginCheck()
{
globalmpList = getmpList(); // Get the list of Management Points for this Site serverif (!mpcheckRunning)
{
try
{
mpcheckStop = false;if (dompCheck.IsBusy != true)
{
dompCheck.RunWorkerAsync();
}
}
catch (Exception ee)
{}
}
}
It should look like this:
Note: You may get warned that getmpList method doesn’t exist, we’re going to create it soon, and until we’ve laid out all the code the project won’t compile properly.
The beginCheck method is doing the following:
- Gets a list of Management Points from the target device
- Checks if the dompCheck thread is already running
- Resets mpcheckStop and mpcheckRunning triggers
- Starts the dompCheck BackgroundWorker thread
Since the thread we’re going to use to check the Management Points is configured, we can move onto coding the underlying methods that represent the events.
Key activities that we want to achieve for the Management Point checking thread are:
- Connect to WMI Namespace on a destination device
- Get the Name and Site Code of the first SMS Provider found
- Connect to the SMS Provider
- Retrieve a list of Management Points, their Site Code and their security type (HTTP\HTTPS)
- Test each Management Point and determine its health state
- Show the result in the DataGridView
So let’s begin designing some structure around that, while keeping an eye on modularity, dispersing tasks to different methods so that we can invoke them multiple times if needed. I prefer spinning things out into methods that I can invoke, it makes for more readable code and reduces having to multiply code in logic blocks, just call the method in multiple places instead.
We’ll create a new method now called checkMP, this will contain the HTTP code to test a Management Point, and is modularised so that we can invoke it from another method for each Management Point discovered.
- Add the following code below the beginCheck method that you created previously:
public string checkMP(string mpName, int mpPort)
{
String httpresponseText = String.Empty;try
{
string connString = "HTTP://" + mpName + ":" + mpPort + "/sms_mp/.sms_aut?mplist";if (!mpcheckStop)
{
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(connString);request.Timeout = 5 * 1000; // 2 Second time out
request.Method = "GET";
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
Stream dataStream = response.GetResponseStream();
StreamReader reader = new StreamReader(dataStream);
httpresponseText = reader.ReadToEnd();
reader.Close();
dataStream.Close();
}
}
}
catch (Exception e)
{
return e.Message; // An error, return the lot!
}if (httpresponseText.Contains("<MPList>"))
{
return "Healthy"; // Healthy
}if (httpresponseText.Contains("The operation has timed out"))
{
return "Timed out"; // Timed out
}return httpresponseText; // Most likely an error, return the lot!
}
It should look like this:
- This method is doing the following:
- Checks if the threads stop trigger is set and jumps out if it is
- Creates a HttpWebRequest object
- Forms up the URL to be used
- Sets the timeout to 5 seconds (5 * 1000 milliseconds)
- Handles the response, healthy, time out, or an error
- Notice that we break HTTPS checks because we hardcode HTTP to the front of the URL that we form up. If we wanted it to work with HTTPS Management Points we’d need to handle a few extra things anyway, this is definitely something someone else could do what with the source code for this project being publically available for modification.
Next up is the method handling the WMI communications. We’ll use it to get a list of Management Points from WMI on the Site server, and pass them back to whoever called the method as a ManagementPoint Collection class, so that we can loop through the Collection calling checkMP each time.
There are at least two ways of handling WMI queries for ConfigMgr, use the Microsoft Configuration Manager Class DLL’s, which you embed into your project, they contain a bunch of code for handling connection and querying of the SMS Provider, or use a .Net WMI ManagementScope class to connect to WMI on a Site server, and query for the SMS Provider so that we can obtain its server name along with the Site code to begin querying it.
- To use the ManagementScope class we need to add System.Management in the Projects references
- Enter system.management or scroll through the list to find it, and Tick it so that its added to the project
Next we’ll create a method that we’ll use to update the StatusStrip, which will be used to report back errors during operation.
- Add the following code below the checkMP method that you created previously:
private void logMessage(string theMessage)
{
ssl_Entry.Text = theMessage;
ss_Messaging.Refresh();
}
It should look like this:
Note that we must never call this from a BackgroundWorker thread or we’ll create a wormhole (it’ll barf).
Next up is a method that converts a Base64 encoded string into a Bitmap image, quite handy for storing a Bitmap inside the project and not depending on an external file for it. We could add it to the project as a Reference but I prefer to encode and store them away like this.
- Add the following code below the logMessage method that you created previously:
private Bitmap loadimagefromString(string Image)
{
try
{
byte[] imageBytes = Convert.FromBase64String(Image);MemoryStream ms = new MemoryStream(imageBytes);
Bitmap streamImage = (Bitmap)Bitmap.FromStream(ms, true);
return streamImage;
}
catch (Exception ee)
{}
return null;
}
It should look like this:
And now we create the getmpList method.
- Add the following code below the loadimagefromString method that you created previously:
private ManagementPointCollection getmpList()
{
ManagementPointCollection mpCollection = new ManagementPointCollection();ManagementScope scope = new ManagementScope(@"\\" + tb_Server.Text + @"\root\SMS");
SelectQuery query = new SelectQuery("select * from SMS_ProviderLocation");
try
{
string smsproviderserverName = String.Empty;
string smsprovidersiteCode = String.Empty;using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query))
{
try
{
ManagementObjectCollection smsProviders = searcher.Get();foreach (ManagementObject smsProvider in smsProviders)
{
smsproviderserverName = smsProvider["Machine"].ToString();
smsprovidersiteCode = smsProvider["SiteCode"].ToString();break; // Get only the first SMS Provider listed, we could do better here
}
}
catch (Exception e)
{
logMessage("Error connecting to Site server - " + e.Message);
}
}if (smsproviderserverName != String.Empty) // Do not proceed if we haven't got a server
{
scope = new ManagementScope(@"\\" + smsproviderserverName + @"\root\SMS\Site_" + smsprovidersiteCode);query = new SelectQuery("select * from SMS_SCI_SysResUse where RoleName like " + (char)34 + "%" + "SMS Management Point" + "%" + (char)34);
using (ManagementObjectSearcher searcher2 = new ManagementObjectSearcher(scope, query))
{
try
{
ManagementObjectCollection mpList = searcher2.Get();foreach (ManagementObject mp in mpList)
{
ManagementBaseObject[] properties = null; // Handle the SMS_EmbeddedProperty arrayproperties = (ManagementBaseObject[])mp["Props"];
bool isHTTPS = false;
foreach (ManagementBaseObject property in properties)
{
if (property["PropertyName"].ToString() == "SslState")
{
isHTTPS = Convert.ToBoolean(property["Value"]);break;
}
}smsproviderserverName = mp["NetworkOSPath"].ToString().Remove(0, 2).ToLower();
ManagementPoint addMP = new ManagementPoint();
addMP.Name = smsproviderserverName;
if (isHTTPS) addMP.Port = 443; else addMP.Port = 80; // Set Port 443 for HTTPS if the MP is configured for SSL, or Port 80 for HTTP
addMP.SiteCode = mp["SiteCode"].ToString();
addMP.State = String.Empty;mpCollection.Add(addMP); // Add our MP to the MP Collection
}
}
catch (Exception e)
{
logMessage("Error handling WMI - " + e.Message);
}
}
}
else
{
logMessage("Could not find an SMS Provider");
}
}
catch (ManagementException e)
{
logMessage("Fatal error - " + e.Message);
}return mpCollection;
}
It should look like this:
Essentially all our WMI interrogation code is in there, we return back a ManagementPoint Collection containing all the Management Points that were discovered. Note that we store the resulting health state from checkMP back into the ManagementPoint object before the collection is returned to the calling method.
We’ll now create a method called checkMPS, from which we’ll iterate through our globalmpList ManagementPoint Collection, and run checkMP for each time.
- Add the following code below the getmpList method that you created previously:
public void checkMPS()
{
foreach(ManagementPoint mp in globalmpList)
{
string returnedState = checkMP(mp.Name, mp.Port);mp.State = returnedState; // We have the result, store it back into this ManagementPoint class instance
if (mpcheckStop) break;
}
}
It should look like this:
Next up are the event classes for dompCheck and doScheduling.
- Add the following code to the dompCheck_DoWork method:
mpcheckRunning = true; // Notify that we are running
BackgroundWorker worker = sender as BackgroundWorker;
if ((worker.CancellationPending == true))
{
e.Cancel = true;
}if (!mpcheckStop)
{
checkMPS();
}
It should look like this:
In this method we notify that the thread is running, check if it needs to be stopped, then kick off the checkMPS method which results in the globalmpList being updated for us.
There is no need to modify the dompCheck_ProgressChanged method as we’re not sending status or state back to the foreground thread from dompCheck.
- Add the following code to the dompCheck_RunWorkerCompleted method:
dgv_Mp.Rows.Clear(); // Clear the dgv_Mp rows
Bitmap stateIcon = loadimagefromString(unhealthyIcon); // Default to unhealthy state icon
foreach (ManagementPoint MP in globalmpList) // Iterate our global MP list
{
if (MP.State.ToLower().Contains("healthy"))
{
stateIcon = loadimagefromString(healthyIcon); // Change to healthy state icon
}dgv_Mp.Rows.Add(MP.Name, MP.SiteCode, stateIcon, MP.State); // Add the MP to dgv_Mp
}mpcheckRunning = false; // Notify that we are finished
mpcheckStop = false; // If we were forced, reset the trigger
b_Go.Text = "Check Management Point"; // Change the b_Go Button text back
It should look like this:
I’ve commented the above code well enough to explain what is happening, but a recap is that we’re clearing the dgv_Mp DataGridView and populating it with the information stored in the ManagementPoint objects hanging out in the globalmpList.
I can see we’re real close to wrapping up here, so let’s crack on.
- Add the following code to doScheduling_DoWork method:
BackgroundWorker worker = sender as BackgroundWorker;
if ((worker.CancellationPending == true))
{
e.Cancel = true;
}DateTime nextCycle = DateTime.UtcNow;
nextCycle = nextCycle.AddMinutes(nudtimerMinutes);
while (1 == 1) // Enter an eternal loop!
{
if (timerStop) break; // Quick! Come this way to get out of the loop!Thread.Sleep(1000); // Sleep for one second
int compareResult = DateTime.Compare(nextCycle, DateTime.UtcNow);
if (compareResult < 0) // Time to trigger a Management Point check
{
worker.ReportProgress(0, ""); // We just want to fire the ProgressChanged event, we do not have anything to pass to itDateTime newCycle = DateTime.UtcNow; // Get current Date and Time
newCycle = newCycle.AddMinutes(nudtimerMinutes); // Add nudtimerMinutes to newCycle
nextCycle = newCycle; // Set nextCycle so that we can fire again
}
}
It should look like this:
What we’re doing above is creating a infinite loop, and from within it we are sleeping for a second, and checking if we’re supposed to invoke a Management Point check. We use DateTime and juggle things around a bit, and could have slept for the entire period, but I wanted the thread to be responsive to requests to stop. We actually get the dompCheck thread started by using the BackgroundWorker ReportProgress event, telling the thread that we want to report some progress back, and from the ProgressChanged method we invoke the beginCheck method.
- Add the following to the doScheduling_ProgressChanged method:
if (!mpcheckRunning)
{
beginCheck(); // Start the Management Point health state check thread
}
It should look like this:
As you can see, we check to see if the dompCheck thread is running, if it isn’t we call beginCheck which will start it for us.
- Add the following to the doScheduling_RunWorkerCompleted method:
timerStop = false; // Reset the threads stop trigger
timerRunning = false; // Declare the thread finished
It should look like this:
Now return to the Form view, and double click the cb_Timer Checkbox control. It’ll return you to Code view and create the cb_Timer_CheckedChanged method for you:
- Add the following to the cb_Timer_CheckedChanged method:
if (cb_Timer.Checked) // User has enabled the scheduler
{
if (!timerRunning)
{
timerStop = false; // Reset the threads stop triggerif (doScheduling.IsBusy != true) // Start the scheduling thread
{
doScheduling.RunWorkerAsync();
}
}
}
else // User has disabled the scheduler
{
timerStop = true; // Stop the scheduling thread
}
It should look like this:
From this method we kick off the doScheduling BackgroundWorker thread or stop it depending on if you tick\untick the Checkbox.
Return back to the form view, double click the nud_timerMinutes control.
- Add the following to nud_timerMinutes_ValueChanged method:
try
{
nudtimerMinutes = Convert.ToInt16(nud_timerMinutes.Value); // Keep the global nudtimerMinutes variable up to date
}
catch (Exception ee)
{}
It should look like this:
When the user makes a change to the value for the nud_timerMinutes control, we’ll change the global nudtimerMinutes variable to reflect the change, keeping them in sync.
Here comes our last block of code, return to Form view and double click the b_Go control.
- Add the following code to the b_Go method:
if (!mpcheckRunning)
{
b_Go.Text = "Stop";beginCheck(); // Start the thread
}
else // Stop the thread
{
mpcheckStop = true;
}
It should look like this:
That’s it. Now press Ctrl+Alt+B to compile the code. If you fitted this together properly you'll get success. On receiving Success, press F5 to run the application, test it out.
Once you point it at a Site server It should look like this:
Well, that was an epic guide!
Not only did we cover a stack of techniques that you can reuse for most of your projects, but we ended up with a tool that’s available on the TechNet Gallery here.
This wraps up this guide, sorry for the lengthy gap between posts, I think this one stretched across a whole year! At least we got there, and as you can see this last post took a lot of time to put together, and is why I was lagging behind doing it hehe. I’ll put together another development related guide soon, focusing more on using the ConfigMgr SDK, suggestions for guides always welcome.
I hope you’ve got something useful from this guide, at worst a working development environment, and a full blown C# project to act as an example for you to plunder as you build out your own projects.
Enjoy.
Robert Marshall – Enterprise Mobility MVP – Director and Principle consultant of SMSMarshall Ltd