What's the right way to handle multiple Windows Forms in a C# program?
Matthew Martin
5/23/2015 10:16:00 PM
Tweetable
There's one issue with Windows Forms that seems needlessly complicated: how to handle
Form.Close
versus Application.Exit
so that it does the right one. Consider this form--it's just the default form with a menustrip added with the default elements:Ok, so consider the first and last menu item shown. When the user selects Exit, we obviously want to completely exit the program, releasing all of the resources back to the operating system--that is, we want to call Application.Exit();
--while when the user selects New we want to close this form and open a completely new form. So we put in the event handlers for code something like this:using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace FormClosingExample
{
public partial class Example : FormMaster
{
public Example()
{
InitializeComponent();
}
private void exit_Click(object sender, EventArgs e)
{
Application.Exit();
}
private void new_Click(object sender, EventArgs e)
{
Example newform = new Example();
newform.Show();
this.Close();
}
}
}
Now, there are multiple problems with this. The first, most obvious problem is that because we are creating the new instance of the Example
form as a child inside of the original one, when we call this.Close();
the program Program
class, which will terminate the application anyway.), including the new form we wanted to open. Ok, so we can substitute instead this.Hide();
so that the old form will no longer be open, but the new form won't be terminated. And when, on the new form, the user selects File->Exit from the menu the method will call Application.Exit();
which will close both the new form and the original hidden one. Everything works.
Well, everything works right up until the moment the user clicks that X button in the top right corner instead of File->Exit. Clicking that button will trigger
this.Close();
on which ever form it is clicked, so if the user clicks it on the new form it will close the new form but won't close the original instance of the form that we had hidden. As a result the application won't exit, and that original form will continue to consume memory and resources in the background until the user shuts the computer down. I see this error committed by inexperienced programmers with surprising frequency. Unfortunately, you can't handle the X button directly--the only handlers that attach to it are the form close and closing events generally, which are also triggered by this.Close();
. Hence, inserting Application.Exit();
in those handlers will cause the application to exit in situations where we want it to stay running. Also unfortunately, the sender
and EventArgs
objects don't provide us much information at all--you could discern which form is calling the event, but not whether it was triggered by the X button specifically--so writing a method here doesn't really work.
This is just annoying. To further understand the problem, let's turn to a somewhat obscure little file that Visual Studio inserted along with our Example form: the file called
Program.cs
. Every executable program in C# has a program Main
method somewhere, which serves as the entry point for the system to start executing code. This is, in fact, what distinguishes an exe program from a dll library which can have all the same code, but lacks a defined starting point. The default contents of the Program.cs
:using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace FormClosingExample
{
static class Program
{
///
/// The main entry point for the application.
///
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Example());
}
}
}
So here's what's happening. You have a mass of compiled code; when you instruct the operating system to run this code, it looks for the Program.Main()
and starts executing statements there, in order from top to bottom, closing the program and returning all resources to the operating system once the end of the Main
is reached. And this gets at the bigger problem with our original code above: even if we divined a way to open a new form that wasn't a dependent of the original form, when we Close();
the original form control will be returned not to the new form but to Program.Main
where, finding nothing left to do, the system will exit the entire application. That is, this.Close();
in the original form is exactly equivalent to Application.Exit();
.
I have not found any great solution to this--I would imagine commonplace--problem. But here's what I've come up with so far. We'll create two static fields in the
Program
that contain two states, the first indicating whether the application should exit when control reverts to Program.Main
, and the second storing an instance of the next form to open if the application is not intended to exit. Our Program.cs file now looks like this:using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace FormClosingExample
{
static class Program
{
static internal bool close = true;
static internal Form open;
///
/// The main entry point for the application.
///
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Example());
if (!close)
{
Application.Run(open);
}
}
}
}
So instead of what we did originally, this is how we handle the New option on the Example
form:private void new_Click(object sender, EventArgs e)
{
Program.open = new Example();
Program.close = false;
this.Close();
}
When the user select File->New, instead of creating a new form as a child of the original one, we've passed the new form to Program
and set Program.close
to false so that, when we call this.Close();
and control returns to Program.Main
, rather than exit the application it will run the new instance of Example
we sent it.
By itself, this seems to work though I'm not totally sure if it should. When we call
Application.Run(open);
it seems that the Program.open
and Program.close
reset to their original states. I'm at a loss to explain why that's true, so I'll just add that inserting Program.close=true;
into the constructors in all our forms--or perhaps into our base FormMaster
class from which all the forms inherit--we can ensure that the state resets. And that takes us to the bonus material:
Bonus Material: A method for database operations
You may have noticed all my forms inherit fromFormMaster
instead of the default Form
. Here's why.
Whether you are writing a forms application or a web application, chances are it will make heavy use of databases. Everything has a database these days--if it isn't interfacing with a full-fledged SQL database service, then it has it's own local database for app data. Here's the C# code to retrieve a (salted hash of a) user's password from a database:
SqlConnection con=new SqlConnection(@"Server=000.000.000.000;Database=exampleDB;User ID=username; Password=pass; Trusted_Connection=False");
string query=@"SELECT hashedPassword FROM Credentials WHERE username=@user;";
SqlDataAdapter adapter=new SqLDataAdapter();
DataSet ds=new DataSet();
con.Open();
adapter.SelectCommand=new SqlCommand(query, con);
adapter.SelectCommand.Parameters.AddWithValue("@user",username);
adapter.Fill(ds);
con.Close();
string password=ds.Tables[0].Rows[0]["hashedPassword"].ToString();
Holy cow that's a lot of crap just to look up a password for a username! What's even more frustrating, though, is that the code for basically every dataretrieval operation is the same. In every case you are just inputting a query and parameters [note: always parameterize any user inputs. DO NOT CONCATENATE QUERIES. Parameters protect against injection attacks that could compromise your data. Despite being 100 percent preventable, SQL injection remains the most common and most deadly type of data security breach.] and outputting a datatable.
Now, one approach is to create a class specifically for doing database operations and create an instance of this database utility class whenever we want to do any database operations in any of our other classes. This seems to be fairly popular. But really, creating an instance of our database utility class every time we want to do a database operation is kind of a waste because our database queries never actually change the internal state of our utility object. If the internal state can't change, then don't instance--that's what I always say. So we could make our database utility class static with the
SqlConnection
as a static field and a static Select
method that accepts the query and parameters as arguments.
To be honest, this seems like bad class-oriented programming to me. We don't want spaghetti classes where all of the classes are calling methods in other classes all the time. Moreover, we don't necessarily want to grant all of our classes the ability to use our database--it's best to keep scope as narrow as possible in general, and restriction of access may be necessary for security reasons. I think the better approach is to put our database utilities into a base class from which the classes that need to access the database inherit. So, for example, our forms which need to retrieve data from the database can inherit from this
FormMaster
class:using System;
using System.Data.SqlClient;
using System.Windows.Forms;
namespace FormClosingExample
{
class FormMaster:Form
{
SqlConnection con=new SqlConnection(@"Server=000.000.000.000;Database=exampleDB;User ID=username; Password=pass;Trusted_Connection=False");
protected void Select(string query, ref dt, params object[] sqlParameters)
{
SqlDataAdapter adapter=new SqLDataAdapter();
DataSet ds=new DataSet();
con.Open();
adapter.SelectCommand=new SqlCommand(query, con);
for(int i=0;i < sqlParameters.Length;i+=2)
{
adapter.SelectCommand.Parameters.AddWithValue(sqlParameters[i].ToString(),sqlParameters[i+1]);
}
adapter.Fill(ds);
con.Close();
dt=ds.Tables[0]
}
}
}
Thus, in any of our other forms we can generically query the database merely by specifying the query, the output datatable, and parameters:
string query=@"SELECT hashedPassword FROM Credentials WHERE username=@user;";
DataTable dt=new DataTable();
Select(query,ref dt,"@user",username);
string password=dt.Rows[0]["hashedPassword"].ToString();
I really like C#'s params
declaration. This way, the method will work with any number of parameters, including no parameters, and we can enter them each directly in the method's arguments, or we can make an array of parameters and then pass the array as an argument to the method.
Obviously we can make more methods in the base class to do other SQL operations, or make them more specific to the things we want the descendant classes to do.