Click here to Skip to main content
15,883,940 members
Articles / Programming Languages / C#

MDI Case Study Purchasing - Part X - Smart Saving

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
14 Dec 2015CPOL5 min read 11.3K   254   5   4

Introduction

In Part X we will deal with Smart Saving. If the user tries to close a document with unsaved changes, we will alert the user, giving them an opportunity to save. We will also deal with this for when the main application is closed. Additionally, we will learn how limit our application instance count to 1, so that if the user runs our application while an instance is already open, we will cancel the new instance, and switch the user to the already running instance.

Smart Document Saving

First, let's deal with simple form closure. If the user tries to close a form that has changed since the last save, or if the form has not yet been saved, we will alert the user and give them an opportunity to either save, not save and let the form close anyway, or cancel the form closure. To do this, we need to override the OnFormClosing method inside our PurchaseOrderForm class. In this method, we will check the state of our Saved boolean, and if it's false, we will show a MessageBox to the user, with Yes, No, and Cancel buttons. If the user clicks Yes, we want to save, No means let the form close unsaved, and Cancel means let's cancel closing the form. Here is what our override should look like

C#
protected override void OnFormClosing(FormClosingEventArgs e)
{
    if (!Saved)
    {
        DialogResult answer = MessageBox.Show(this.Text + 
                                              " has been changed. Do you wish to save\nbefore closing?",
                                              "Unsaved Changes", MessageBoxButtons.YesNoCancel, 
                                              MessageBoxIcon.Exclamation);
        switch (answer)
        {
            case System.Windows.Forms.DialogResult.Yes:
                Save();
                if (!Saved) e.Cancel = true;
                break;
            case System.Windows.Forms.DialogResult.Cancel:
                e.Cancel = true;
                break;
        }
    }
    base.OnFormClosing(e);
}

In the Yes case, after calling Save() we want to recheck the Saved value, and if it's still false, we want to cancel closure. This accounts for the user cancelling the SaveFile dialog. Now we need to modify our Save() method so that it will prompt the user with a SaveFile dialog in case the document has not yet been saved.

C#
public void Save()
{
    if (_fileName != null)
    {
        SaveAs(_fileName);
    }
    else
    {
        SaveFileDialog saveFileDialog = new SaveFileDialog();
        saveFileDialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
        saveFileDialog.Filter = "Purchase Orders (*.pof)|*.pof";
        saveFileDialog.DefaultExt = ".pof";
        saveFileDialog.AddExtension = true;
        if (saveFileDialog.ShowDialog(this) == DialogResult.OK)
        {
            String FileName = saveFileDialog.FileName;
            this.SaveAs(FileName);
        }
    }
}

Next, let's account for instances when the user closes the application from the task bar icon. We need to go to MDIForm and make a couple of changes. We need to add a public boolean, we'll call it UnsavedDocuments, that will report whether there are any open child forms that are not saved. So in MDIForm let's add that boolean

C#
public bool UnsavedDocuments
{
   get
   {
      foreach(PurchaseOrderForm f in this.MdiChildren)
      {
         if (!f.Saved) return true;
      }
      return false;
   }
​}

And now we need to add a public method to forceably attempt to close each open form, so that it's FormClosing event will fire

C#
public void CloseAllDocuments()
{
   foreach(Form f in this.MdiChildren) f.Close();
​}

Now let's move to SplashForm. Here we need to override the OnFormClosing method, where we will call CloseAllDocuments() and then check the value of UnsavedDocuments, and if it's still true, we will cancel the close. We also want to present the MDIForm back to the user

C#
protected override void OnFormClosing(FormClosingEventArgs e)
{
   if(_mainForm != null && _mainForm.UnsavedDocuments)
   {
      if (_mainForm.WindowState == WindowStates.Minimized) _mainForm.WindowState = WindowStates.Normal;
      _mainForm.Show();
      _mainForm.CloseAllDocuments();
      if (_mainForm.UnsavedDocuments) e.Cancel = true;
   }
   base.OnFormClosing(e);
}

Now if the user tries to close the application with the task bar icon, and unsaved documents are open, the user will now be alerted. You'll notice now though, if you click to close MDIForm with unsaved documents, even though we are just hiding, we still get the FormClosing event from all unsaved forms. We can fix that, by checking the reason for closure in the OnFormClosing method. If the CloseReason is MdiFormClosing, we can elect to Cancel the close event. Change the OnFormClosing method in PurchaseOrderForm to

C#
protected override void OnFormClosing(FormClosingEventArgs e)
{
    if (e.CloseReason == CloseReason.MdiFormClosing)
    {
        e.Cancel = true;
    }
    else if (!Saved)
    {
        DialogResult answer = MessageBox.Show(this.Text + 
                                              " has been changed. Do you wish to save\nbefore closing?",
                                              "Unsaved Changes", MessageBoxButtons.YesNoCancel, 
                                              MessageBoxIcon.Exclamation);
        switch (answer)
        {
            case System.Windows.Forms.DialogResult.Yes:
                Save();
                if (!Saved) e.Cancel = true;
                break;
            case System.Windows.Forms.DialogResult.Cancel:
                e.Cancel = true;
                break;
        }
    }
    base.OnFormClosing(e);
}

Now, since closing the MDIForm itself always just hides the form, we won't be pestered with unsaved alerts in that instance. So that does it for Smart Saving

Limiting Application Instance Count To 1

To achieve this, we will go to our Program class, and look at all running processes to see if any are identical to our process, and if so we will switch to it. To switch to the currently open instance, we will again take advantage of the WndProc method to capture a customer message that we will broadcast out before we shut down the newly opening instance. To send the message we will use the User32.SendMessage method. The User32 class is already included with the source, and handles our User32.dll imports. First we need to set up a couple of variables in the Program class. HWND_BROADCAST will be the IntPtr for the Broadcast handle, so that our message gets to all running processes, inluding the originally opened instance of our application.

C#
private const int HWND_BROADCAST = 0xFFFF;

Next let's define our custom message by registering it with the Windows messaging system. To do this, we will call the User32.RegisterWindowsMessage method

C#
public static readonly int WM_MDIACTIVATEAPP = User32.RegisterWindowMessage("WM_MDIACTIVATEAPP");

Add a using statement for System.Diagnostics, and then change your Main() method to

C#
static void Main()
{
    Process thisProcess = Process.GetCurrentProcess();
    foreach (Process p in Process.GetProcessesByName(thisProcess.ProcessName))
    {
        if (p.Id != thisProcess.Id)
        {
            User32.SendMessage((IntPtr)HWND_BROADCAST, WM_MDIACTIVATEAPP, IntPtr.Zero, IntPtr.Zero);
            return;
        }
    }
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new SplashForm());
}

Now if you try it out, you'll see, the application will not allow a second instance to open. However, rather than just ignore a second opening, we want our application to respond by presenting the already open instance back to the user. This time we want to override the WndProc method in SplashForm

C#
protected override void WndProc(ref Message m)
{
    if (m.Msg == Program.WM_MDIACTIVATEAPP)
    {
        if (_mainForm != null)
        {
            if (_mainForm.WindowState == FormWindowState.Minimized)
            {
               _mainForm.WindowState = FormWindowState.Maximized;
            }
            _mainForm.Show();
            _mainForm.BringToFront();
        }
    }
    base.WndProc(ref m);
}

Now we are responding to the custom Windows message we broadcasted from the newly opening instance, and can present the UI back to the user. 

Mutex

The method outlined above does have a few drawbacks. The biggest is that it has a possiblilty of getting caught in a race condition, especially if your application is a multi-user application. Another way to handle this, is to use the System.Threading.Mutex object.

C#
using System.Threading;
......
public static Mutex AppMutex { get; set; }
static void Main()
{
   String mutexName = String.Format("Local\\{0}Mutex_{1}_{2}", Application.ProductName,
                                    Environment.UserDomainName, Environment.UserName);
   bool secondInstance = false;
   AppMutex = new Mutex(true, mutexName, out secondInstance);
   if (!secondInstance)
   {
      User32.SendMessage((IntPtr)HWND_BROADCAST, WM_MDIACTIVATEAPP, IntPtr.Zero, IntPtr.Zero);
      return;
   }

   Application.EnableVisualStyles();
   Application.SetCompatibleTextRenderingDefault(false);
   Application.Run(new SplashForm());
}

The mutexName variable, we started it with the prefix "Local\", and put the current user domain and user name into it. This allows multiple instances per machine, but only one per user. If you wanted to limit the instances to one per machine, you could instead define your mutexName as 

C#
String mutexName = String.Format("Global\\{0}Mutex", Application.ProductName);

That does it for Part X. I hope this guide has been helpful to some of it's readers. This concludes the basics. In upcoming installments we will incorporate docking panels, PDF rendering, Emailing, and a few other tecniques. 

Points of Interest

  • Smart Saving
  • Instance Limiting
  • Windows Messaging
  • Mutex

History

Keep a running update of any changes or improvements you've made here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
BugThere is a typo in your posted code Pin
HiDensity29-Dec-15 8:55
HiDensity29-Dec-15 8:55 
Hey stebo0728!

First of all: This is a really good tutorial, covering a lot of things, you may come across when dealing with MDI applications!

But you have a slight typo in your text and pasted code: You are writing of "RegisterWindowsMessage" in this article. But the correct method's name would be "RegisterWindowMessage" - without an "s" after "Window". In your "User32.cs" source code it is spelled correctly.
If someone is using your posted code and does an automatically renaming of the methods, he will run into an error, not able to debug or anything else.
Would be great, if you could fix this issue.
GeneralRe: There is a typo in your posted code Pin
stebo072829-Dec-15 11:08
stebo072829-Dec-15 11:08 
GeneralMy vote of 5 Pin
PVX00717-Dec-15 11:05
PVX00717-Dec-15 11:05 
GeneralMy vote of 5 Pin
JayantaChatterjee16-Dec-15 19:10
professionalJayantaChatterjee16-Dec-15 19:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.