Friday, September 01, 2006 3:53 PM bart

MMC 3.0 - A managed code 'task manager' MMC 3.0 snap-in

Introduction

A while ago, MMC 3.0 was released to the web. Together with Windows PowerShell these technologies make up the corner stones of next-generation manageability, something that will improve even more in the future with the "Aspen" project.

Although MMC 3.0 documentation is online on MSDN for quite a while, the SDK hasn't been published yet. The Microsoft Platform SDK for Windows Server 2003 R2 still comes with MMC 2.0 documentation, but the good news is that the new Windows SDK ships with MMC 3.0 development documentation and a bunch of great samples.

In this post I'll show you how to create a simple "task manager" MMC 3.0 snap-in using managed code (C# to be more specific). Some useful system information:

  • Development machine: Windows Vista build 5472 with Visual Studio 2005 and .NET Framework 3.0 installation.
  • Test machine: Windows Vista build 5536 (pre-RC1) running on Virtual Server 2005 R2 SP1.

Getting started

  1. Basically, a managed code MMC 3.0 snap-in is nothing more than an assembly with System.Configuration.Install.Installer-support. So, we'll start by creating a new Class Library project in Visual Studio 2005.
  2. Now comes the somewhat tricky part of referring to Microsoft.ManagementConsole.dll. This assembly can be found in the GAC (%windir%\assembly) but doesn't appear in the Visual Studio 2005 Add Reference dialog by default. There are a few possible solutions to this, like customizing the Add Reference dialog. I'll do a little cmd-lining however:

    C:\Users\Bart>cd %windir%\assembly\GAC_MSIL\Microsoft.ManagementConsole\3.0.0.0__31bf3856ad364e35

    C:\Windows\assembly\GAC_MSIL\Microsoft.ManagementConsole\3.0.0.0__31bf3856ad364e35>copy Microsoft.ManagementConsole.dll c:\temp
            1 file(s) copied.



  3. Now, add a reference to the copied Microsoft.ManagementConsole.dll assembly:


  4. Finally, rename Class1.cs to SampleSnapIn.cs.

The basics of a managed code MMC 3.0 snap-in

Creating a managed code MMC 3.0 snap-in isn't difficult at all. Here's the list of must-do's:

<Intermezzo Title="Generating GUIDs in Visual Studio 2005">

Unique identifiers, that's what GUIDs are for. But how to generate them? One possible solution is of course to call Guid.NewGuid() and create some application that uses System.Windows.Forms.Clipboard to put the newly generated GUID on the clipboard:

using System;
using
System.Windows.Forms;

class
GuidGen
{
   [
STAThread
]
   public static void
Main()
   {
      Console.Write("GUID-to-clipboard... "
);
      Clipboard.SetText(System.
Guid.NewGuid().ToString("B"
).ToUpper());
      Console.WriteLine("Done"
);
   }
}

Not too bad; just give it a try if you have some time left :-). However, the good news is that Visual Studi 2005 comes with a built-in tool called GuidGen.exe that can be found in the Common7\Tools folder (the where.exe command ships with Windows Server 2003 and Windows Vista):

Setting environment for using Microsoft Visual Studio 2005 x86 tools.

C:\Program Files\Microsoft Visual Studio 8\VC>where guidgen.exe
C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\guidgen.exe
C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\Bin\GuidGen.exe

This tool was included with Visual Studio .NET 2003 too with the little advantage of having it linked in the Tools menu of the IDE by default. This is not the case in Visual Studio 2005 but you can get around this by adding the menu entry yourself:

  1. Go to Tools, External Tools...
  2. Choose Add and change [New Tool1] to:
    • Title: Create &GUID
    • Command: <full path to guidgen.exe on your system>
    • Initial directory: <path where guidgen.exe lives>
  3. You're done

</Intermezzo>

Here's the code for our basic snap-in so far:

using System;
using
System.ComponentModel;
using
System.Diagnostics;
using
Microsoft.ManagementConsole;

namespace
SampleMmcSnapIn
{
   [
RunInstaller(true
)]
   public class SampleMmcSnapInInstall :
SnapInInstaller
   {
   }

   [SnapInSettings("{00ECA916-9A13-426e-8D1F-89C432177FD4}", DisplayName="MMC 3.0 to the max", Description="A first MMC 3.0 example")]
   public
class SampleSnapIn : SnapIn
   {
      public
SampleSnapIn()
      {
         this.RootNode = new ProcessScopeNode
();
      }
   }
}

Trees have roots

There's an unknown in the code above: the ProcessScopeNode. Every MMC snap-in has a RootNode that corresponds - what's in a name? - with the root node of the tree that's shown in the left pane of the MMC window. Although you can just work with the Microsoft.ManagementConsole.ScopeNode class (see HelloWorld sample), we'll inherit from it because we need to override some methods (see below):

public class ProcessScopeNode : ScopeNode
{
   public ProcessScopeNode() : base
()
   {
      this.DisplayName = "Process manager"
;
      this.EnabledStandardVerbs = StandardVerbs
.Refresh;

      FormViewDescription viewComputers = new FormViewDescription(typeof(ProcessManagerControl
));
      viewComputers.DisplayName =
"Processes"
;
      viewComputers.ViewType =
typeof(ComputerFormView
);

      this
.ViewDescriptions.Add(viewComputers);
      this
.ViewDescriptions.DefaultIndex = 0;
   }

   protected override void OnRefresh(AsyncStatus
status)
   {
      base
.OnRefresh(status);
      ProcessWatcher
.Refresh();
   }
}

So, what's up in this code?

  • First we set the node's DisplayName to "Process manager". This will show up in the MMC tree.
  • Using EnabledStandardVerbs we set the "standard verbs" (see Microsoft.ManagementConsole.StandardVerbs) to Refresh only (notice the enumeration is one of the flags type so you can combine multiple verbs using the | operator). This will add Refresh to the Actions pane in the MMC window, in order to refresh the process list. When the user clicks on Refresh, the OnRefresh method will be invoked (see further).
  • Next, the "content pane" of the MMC is populated using a so-called ViewDescription. There are four available ViewDescription types: FormViewDescription, HtmlViewDescription, MessageViewDescription and MmcListViewDescription. I've chosen for the first one, because it allows to host a Windows Forms control. The constructor of FormViewDescription takes the type of the Windows Forms control to be shown inside the "content pane" when the view is selected. The ViewType of the FormViewDescription instance will be covered later on. Finally, the available views are added to the ViewDescriptions collection, which allows the user to select the desired view (in our case there's only one and we mark it as the default one using DefaultIndex).

The OnRefresh override is triggered when the user clicks on Refresh and does the following:

   protected override void OnRefresh(AsyncStatus status)
   {
      base
.OnRefresh(status);
      ProcessWatcher
.Refresh();
   }

In here, ProcessWatcher is a helper class:

public static class ProcessWatcher
{
   public static event EventHandler
Update;

   public static void
Refresh()
   {
      EventHandler
e = Update;
      if (e != null
)
         e(
null, EventArgs
.Empty);
   }
}

This is - I confess - some plumbing to update the WinForms control (see further) but I didn't find an easier way yet (it would lead me too far away if I started to nag about failed experiments :-)).

The core functionality

On to the snap-in's core functionality. In the code fragment above you saw this:

      viewComputers.ViewType = typeof(ComputerFormView);

With every FormViewDescription, one needs a FormView. In an analogous fashion, there is an HtmlView, MessageView and MmcListView. Our FormView definition is this:

public class ComputerFormView : FormView
{
   private ProcessManagerControl
control;

   public ComputerFormView() : base
()
   {
   }

   //Occurs when the snap-in is initialized; just load some sample data.
   protected override void OnInitialize(AsyncStatus status)
   {
      base.OnInitialize(status);
      control = (
ProcessManagerControl) this.Control;
      control.RefreshProcessList();

      ProcessWatcher.Update += new EventHandler(ProcessWatcher_Update);
   }


   void ProcessWatcher_Update(object sender, EventArgs e)
   {
      control.RefreshProcessList();
   }

   //Occurs when an action is performed on a selected item (see OnAction for non-selection actions).
   protected override void OnSelectionAction(Action action, AsyncStatus
status)
   {
      base
.OnSelectionAction(action, status);

      switch ((string
)action.Tag)
      {
         case "Kill"
:
            Process
[] processes = control.SelectedProcesses;

            int
n = processes.Length;
            int
success = 0;

            for (int
i = 0; i < n; i++)
            {
               string
process = processes[i].ProcessName;
               status.ReportProgress(i, n,
"Killing "
+ process);

               try
               {
                  processes[i].Kill();
                  if
(processes[i].WaitForExit(5000))
                     success++;
               }
catch { //Eat the exception for demo purposes.
}
            }

            status.Complete(success +
"/" + n + " processes killed."
, success == n);
            control.RefreshProcessList();

            break
;
      }
   }
}

Wow, that's a bunch of code isn't it? No worries, it's pretty self-explanatory. The OnInitialize method is where everything starts (okay, the system will call the mandatory default constructor too, but we left that blank):

   protected override void OnInitialize(AsyncStatus status)
   {
      base
.OnInitialize(status);
      control = (
ProcessManagerControl) this.Control;
      control.RefreshProcessList();

      ProcessWatcher.Update += new EventHandler(ProcessWatcher_Update);
   }


   void ProcessWatcher_Update(object sender, EventArgs e)
   {
      control.RefreshProcessList();
   }

When OnInitialize is called, you can get a reference to the embedded Windows Forms control (see further). The RefreshProcessList call on the control will be explained further on. Once the control is initialized (with the process list), we hook up an event handler for the ProcessWatcher (see above) that will perform a RefreshProcessList when the user clicks on Refresh (recall the ProcessWatcher.Refresh() invocation in the OnRefresh method of the ProcessScopeNode class). The mechanism using the event seemed to be the easiest one because you can't add parameters to the ComputerFormView constructor.

Further on, the is the OnSelectionAction method that's invoked when the user performs an action on a selection of items. The list of available actions will be populated further on inside the Windows Forms control, so hang on for a minute. Based on the Microsoft.ManagementConsole.Action parameter of this method, we decide on the action to take. There will be only one, which is tagged as "Kill". The code to kill the process(es) is not of much interest and just uses the System.Diagnostics.Process.Kill method and waits for the killed process to exit for 5 seconds using System.Diagnostics.Process.WaitForExit. Of more interest is the feedback mechanism using the Microsoft.ManagementConsole.AsyncStatus class. Using this class, the MMC is kept responding while the action is being performed. Using methods like ReportProgress and Complete the status is communicated to the MMC.

Building the bridge with WinFoms

Okay, we've just established the bridge foundation to the Windows Forms control, let's move on to the control itself now. Add a new User Control to the project and call it ProcessManagerControl. Then perform the following design jobs:

  1. Add a ListView control to the form called lstProcesses and set Sorting=Ascending, Dock=Fill, View=Details. Change the Columns property to have two columns: one with Text="Process Name" and Width="150" and one with Text="PID".

The result should be somewhat like this:

Now go to the code-behind (ProcessManagerControl.cs). We'll examine the code piece-by-piece so just go on and copy the code fragments each individually and in order to the class.

Implement the Microsoft.ManagementConsole.IFormViewControl interface and change the constructor:

public partial class ProcessManagerControl : UserControl, IFormViewControl
{
   private FormView
view;

   public
ProcessManagerControl()
   {
      InitializeComponent();
      this.Dock = DockStyle
.Fill;
   }

   #region
IFormViewControl Members

   void IFormViewControl.Initialize(FormView
view)
   {
      this
.view = view;
      view.SelectionData.ActionsPaneItems.Clear();
      view.SelectionData.ActionsPaneItems.Add(
new Action("Kill", "Kills the process", -1, "Kill"
));
   }

   #endregion

The interface's Initialize method passes the associated FormView (see earlier for the implemention of that one, i.e. ComputerFormView) which is kept as a private member. The SelectionData property has a reference to an object of type Microsoft.ManagementConsole.SectionData and acts as kind of a channel between the FormView and the IFormViewControl. The control notifies the FormView about changes whenever selection is changed. In the Initialize method, the Actions are set through the ActionPaneItems property. It's here that you can see the Kill action addition (with a display name, a description, an image index and a tag as constructor parameters).

Next, provide the following additional methods that are used by the ComputerFormView to send commands to and retrieve data from the control:

public void RefreshProcessList()
{
   lstProcesses.Items.Clear();
   foreach (Process process in Process
.GetProcesses())
      lstProcesses.Items.Add(
new ProcessListViewItem
(process));
}

public Process
[] SelectedProcesses
{
   get
   {
      List<Process> result = new List<Process
>();
      foreach (ProcessListViewItem p in
lstProcesses.SelectedItems)
         result.Add(p.process);
      return
result.ToArray();
   }
}

No explanation required, this is just plain vanilla Windows Forms code to interact with the ListView control. Framework design freaks have a point if they say the SelectedProcesses should really be a method called GetSelectedProcesses because of its complex processing job.

This code uses a ProcessListViewItem which is - again - self-made as a nested private class:

private class ProcessListViewItem : ListViewItem
{
   internal Process
process;

   public
ProcessListViewItem(Process process) : base()
   {
      this
.process = process;
      this
.Text = process.ProcessName;
      this
.SubItems.Add(process.Id.ToString());
   }
}

The goal of this class is to keep a reference to the Process object and to do the column formatting (through the SubItems collection) upon construction of a new instance.

Now the last piece of code which requires you to add an event handler for the lstProcesses' SelectedIndexChanged event:

private void lstProcesses_SelectedIndexChanged(object sender, EventArgs e)
{
   view.SelectionData.EnabledStandardVerbs = StandardVerbs.None;

   if
(lstProcesses.SelectedItems.Count == 0)
   {
      view.SelectionData.Clear();
   }
   else
   {
      view.SelectionData.Update(GetProcesses, (lstProcesses.SelectedItems.Count > 1),
null, null
);
      view.SelectionData.DisplayName = (lstProcesses.SelectedItems.Count == 1 ? lstProcesses.SelectedItems[0].Text :
"Selected processes"
);
   }
}

private string GetProcesses()
{
   StringBuilder sb = new StringBuilder();

   foreach (ProcessListViewItem p in lstProcesses.SelectedItems)
      sb.AppendFormat(
"{0}:{1}\n", p.process.ProcessName, p.process.Id);

   return sb.ToString();
}

This is where the communication with the ComputerFormView's SelectionData (the selection context) collection happens in order to display status and update the MMC. The first line of code (view.SelectionData.EnabledStandardVerbs = StandardVerbs.None) tells the MMC that no standard actions should be enabled for selections. You could however enabled actions such as Properties for selections (requires implementation of "property sheets" which falls out the scope of this post).

When no process is selected, the SelectionData is cleared. However, when you select one or more processes, some string indicating the selected processes is communicated through the Update method. In our example, this parameter is not used any further but it would be of use in case we'd implement property sheets for the selection(s). The second parameter of this method indicates whether the selection is a multi-select, whileas the third and fourth parameter are not of use to use right now (these can be used if the processes would be represented as nodes in the tree structure too and if you want to communicate so-called shared data).

Last but not least the DisplayName is changed to reflect the selection (the process name in case of one selected process, some generic text "Selected processes" in case more than one process was selected).

Install and test

Time to compile (CTRL-SHIFT-B) and test it. Go to a Visual Studio 2005 Command Prompt (run as Administrator because the system has to modify HKLM), cd into the bin\Debug folder of your project and installutil.exe the assembly as shown below:

In the registry you should now find a key called HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MMC\SnapIns\FX:{00eca916-9a13-426e-8d1f-89c432177fd4}. (Replace the GUID by the one you've generated using GuidGen.exe.) Notice the FX: prefix which indicates the managed code our snap-in is built with. Settings should look like:

Now it's time to test the snap-in. Open mmc.exe which will ask you for permissions to continue (User Account Control). The MMC 3.0 version looks as follows and has the Action pane on the right:



Now press CTRL-M (or go to File, Add/Remove Snap-in...) and add the "MMC 3.0 to the max" snap-in to the Console Root as shown below:

There you go: the Process manager shows up in the MMC tree and the action list contains the Refresh action. When selecting one or more processes, the Kill action shows up too. Notice the asynchronous character won't show up because Process.Kill works almost instantaneously, but feel free to experiment with some more complex jobs that do require more processing.

Cool isn't it?

Download code and conclusion

MMC 3.0 makes it really easy for managed code developers to create appealing MMC snap-ins to manage their applications. The new manageability methodology that will be pushed with the release of Windows PowerShell goes a little further however, by running MMC 3.0 as the front-end to PowerShell cmdlets behind the scenes (e.g. killing a process would invoke stop-process). That's the approach that Exchange Server 2007 and System Center Operations Manager 2007 will take. I'll blog about combining PowerShell and MMC 3.0 later on, so keep watching my blog.

Download the sample code (including MSI installer project) through the post attachment.

Keep it managed!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under:

Comments

# re: MMC 3.0 - A managed code 'task manager' MMC 3.0 snap-in

Monday, September 04, 2006 11:59 PM by James Hancock

Great article!

How do you debug these suckers?
I'm getting the following error and I don't have a clue on how to go about debugging and fixing it:

at System.Windows.Forms.Control.ControlCollection.Add(Control value)
  at Microsoft.ManagementConsole.FormView.InternalInitialize()
  at Microsoft.ManagementConsole.View.HandleInitializationRequest(IRequestStatus requestStatus)
  at Microsoft.ManagementConsole.View.ProcessRequest(Request request)
  at Microsoft.ManagementConsole.ViewMessageClient.ProcessRequest(Request request)
  at Microsoft.ManagementConsole.Internal.IMessageClient.ProcessRequest(Request request)
  at Microsoft.ManagementConsole.Executive.RequestStatus.BeginRequest(IMessageClient messageClient, RequestInfo requestInfo)
  at Microsoft.ManagementConsole.Executive.SnapInRequestOperation.ProcessRequest()
  at Microsoft.ManagementConsole.Executive.Operation.OnThreadTransfer(SimpleOperationCallback callback)

# re: MMC 3.0 - A managed code 'task manager' MMC 3.0 snap-in

Tuesday, September 05, 2006 6:25 PM by bart

Hi James,

What's the exception you get to see and when does it occur; at "add snap-in" time or when using it? Haven't tried it yet, but did you attach the VS2005 debugger to the mmc.exe instance where you've loaded the snap-in?

-Bart

# Windows Server 2003 and Windows XP Professional x64 SP2 RTW

Wednesday, March 14, 2007 2:47 AM by B# .NET Blog

The latest Service Pack for Windows Server 2003 and Windows XP Pro x64, SP2, is ready to be downloaded

# The managed MMC 3.0 snap-in cookbook

Wednesday, February 27, 2008 12:04 AM by B# .NET Blog

My recent series of &quot;cookbook&quot; posts has been very well-received and coincidentally I got mail

# MMC Snap-In programmieren | hilpers

Tuesday, January 20, 2009 6:39 AM by MMC Snap-In programmieren | hilpers

Pingback from  MMC Snap-In programmieren | hilpers