Tuesday, August 17, 2010

Prompt for Save changes in MMC 3.0 Application (C#)

Microsoft Management Console 3.0 is a managed platform to write and host application inside the standard Windows configuration console. It provides a simple, consistent and integrated management user interface and administrative console. In one of the product I am currently working with uses this MMC SDK. We used it to develop the configuration panel of administrative purposes.

That’s the quick background description of this post, however, this post is not meant for a guy who never worked with MMC SDK. I am assuming you already know all the basics about it.

In our application it has quite a number of ScopeNodes and each of them has associated details pages (in MMC terminology they are View, in our case most of them are FormView ). All of them have application data rendered in a WinForm's UserControl. We allow user to modify those configuration data in place.
But the problem begins when user move to a different scope node and then close the MMC console window. Now the changes the user made earlier are not saved. As you already know that MMC console is an MDI application and the scope nodes are completely isolated from each others. Therefore, you can’t prompt user to save or discard the pending changes.

I googled a lot to get a solution for this, but ended up with a myriad of frustrations. Many people also faced the same problem and later they implemented with a pop-up dialog, so that they can handle the full lifecycle of saving functionalities. But that causes a lot of development works when you have too many scope nodes. You need to define 2 forms for each of them. One for the read-only display, another is a pop-up to edit the data. For my product, it was not viable really. In fact, it’s even looks nasty to get a pop-up for each node configuration.
Anyway, I finally resolved my problem by myself. It’s not a very good way to settle this issue, but it works superb for my purpose. So here is what I did to fix this problem. I have a class that will intercept the Windows messages and will take action while user is trying to close the main Console.



/// Subclassing the main window handle's WndProc method to intercept
/// the close event
internal class SubClassHWND : NativeWindow
{
private const int WM_CLOSE = 0x10;
private MySnapIn _snapIn; // MySnapIn class is derived from SnapIn (MMC SDK)
private List<SubClassHWND> _childNativeWindows;

// Constructs a new instance of this class
internal SubClassHWND(MySnapIn snapIn)
{
this._snapIn = snapIn;
this._childNativeWindows = new List<SubClassHWND>();
}

// Starts the hook process
internal void StartHook()
{
// get the handle
var handle = Process.GetCurrentProcess().MainWindowHandle;

if (handle != null && handle.ToInt32() > 0)
{
// assign it now
this.AssignHandle(handle);
// get the childrens
foreach (var childHandle in GetChildWindows(handle))
{
var childSubClass = new SubClassHWND(this._snapIn);

// assign this
childSubClass.AssignHandle(childHandle);

// keep the instance alive
_childNativeWindows.Add(childSubClass);
}
}
}

// The overriden windows procedure
protected override void WndProc(ref Message m)
{
if (_snapIn != null && m.Msg == WM_CLOSE)
{ // if we have a valid snapin instance
if (!_snapIn.CanCloseSnapIn(this.Handle))
{ // if we can close
return; // don't close this then
}
}
// delegate the message to the chain
base.WndProc(ref m);
}

// Requests the handle to close the window
internal static void RequestClose(IntPtr hwnd)
{
SendMessage(hwnd.ToInt32(), WM_CLOSE, 0, 0);
}

// Send a Windows Message
[DllImport("user32.dll")]
public static extern int SendMessage(int hWnd,
int Msg,
int wParam,
int lParam);


[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr i);

public static List<IntPtr> GetChildWindows(IntPtr parent)
{
List<IntPtr> result = new List<IntPtr>();
GCHandle listHandle = GCHandle.Alloc(result);
try
{
EnumWindowProc childProc = new EnumWindowProc(EnumWindow);
EnumChildWindows(parent, childProc, GCHandle.ToIntPtr(listHandle));
}
finally
{
if (listHandle.IsAllocated)
listHandle.Free();
}
return result;
}

private static bool EnumWindow(IntPtr handle, IntPtr pointer)
{
GCHandle gch = GCHandle.FromIntPtr(pointer);
List<IntPtr> list = gch.Target as List<IntPtr>;
if (list == null)
{
throw new InvalidCastException("GCHandle Target could not be cast as List<IntPtr>");
}
list.Add(handle);
// You can modify this to check to see if you want to cancel the operation, then return a null here
return true;
}

public delegate bool EnumWindowProc(IntPtr hWnd, IntPtr parameter);
}



This class has the "WndProc" - windows message pump (or dispatcher method) method that will receive all the messages that are sent to the MMC console main window. Also this class will set message hook to all the child windows hosted inside the MMC MDI window.

Now we will only invoke the StartHook method from the SnapIn to active this interception hook.



// The subclassed SnapIn for my application
public class MySnapIn : SnapIn
{
internal SubClassHWND SubClassHWND
{
get;
private set;
}

protected MySnapIn()
{
// create the subclassing support
SubClassHWND = new SubClassHWND(this);
}

// Start the hook now
protected override void OnInitialize()
{
SubClassHWND.StartHook()
}


Now we have the option to do something before closing the application, like prompting with a Yes, No and Cancel dialog like NotePad does for a dirty file.



// Determins if the snapin can be closed now or not
internal bool CanCloseSnapIn(IntPtr requestWindow)
{
if (IsDirty)
{ // found a node dirty, ask user if we can
// close this dialog or not
this.BeginInvoke(new System.Action(() =>
{
using (var dlg = new SnapInCloseWarningDialog())
{
var dlgRes = Console.ShowDialog(dlg);

switch (dlgRes)
{
case DialogResult.Yes:
SaveDirtyData(); // save them here
IsDirty = false; // set to false, so next
// time the method
// will not prevent
// closing the application
SubClassHWND.RequestClose(requestWindow);
break;
case DialogResult.No:
IsDirty = false;
SubClassHWND.RequestClose(requestWindow);
break;
case DialogResult.Cancel: break;// Do nothing
}
}
}));
return false;
}
return true;
}




One small problem remains though. The dispatcher method gets the WM_CLOSE in a thread that can’t display a Window due to the fact that the current thread is not really a GUI thread. So we have to do a tricky solution there. We need to display the prompt by using a delegate (using BeginInvoke) and discard the current WM_CLOSE message that we intercepted already.
Later when a choice has been made (user selected yes, no or cancel), if they selected ‘Yes’ then we have to close the application after saving the data. If ‘no’ selected we will have to close the SnapIn as well. Only for ‘Cancel’ we don’t have to do anything. So only thing is critical is how we can close this window again. Here is how we can do that:

Notice that SnapIn’s CanCloseSnapIn method does have a parameter which is the pointer (an instance of IntPtr in this case) of the window handle that has been closed by the user. This has been done on purpose. This will offer the possiblity to send a WM_CLOSE again to that same window. So even if user closes the MDI child it will only close the child window only after save- which is just perfect!

Hope this will help somebody struggling with the same gotcha.

5 comments:

  1. Thank you so much for posting your sample code.

    It helps a lot!!

    ReplyDelete
  2. Pleasure to know it helped you.
    Cheers

    ReplyDelete
  3. Hello
    I am a young student DOT.Net in training course at TOTAL in Australia.
    I seek has to develop on Meridian and I seek the code to open the file zip BlueCieloCielo .NET SDK 1.1.zip /1.2/1.4/1.5
    My tutor my given these files but I n' have any means of using them and in more I do not know much Meridian.
    If you can m' to help I would be very happy
    Thank you
    Tiffany
    tiffany.edern@yahoo.com

    ReplyDelete
  4. Hi Tiffany..Didn't understand exactly what type of help you are expecting from me?
    Please send me a mail with a bit more details.

    My Gmail address is moimhossain.

    ReplyDelete
  5. hi i've developed a snap in and i've to right click on node to load form view.Isn't der a way to load it directly wen i click on node thanks in advance

    ReplyDelete