MDI Case Study Purchasing - Part IV - Events
Introduction
In Part IV, we will work on laying the foundation for events in our application. Creating the events, and the event arguments classes. We will add two events to our PurchaseOrder
, we'll name them Changing
and Changed
. These events will fire any time a property of our document changes. Again for now all we have is the text of our text box, but as we add things to our document, we will make sure changes cause a fire of the Changing
and Changed
events.
Events - Changing & Changed
We will start our events off with adding two events to our PurchaseOrder
class, and we will call them Changing
, and Changed
. These events will fire off any time a property of our PurchaseOrder
changes. An event is a delegate. The easiest way to explain it is to jump right in. First we need to define our event. As we progress we will be creating many events, for organization sake, I typically create one code file to hold all of my events, and one code file to hold all of my event argument classes. Add a new code file to your project, and name it Events.cs. The code file will be completely blank, so you need to add your namespace block. Event delegates always take 2 arguments, the object initiating the event, and the arguments object associated with the event. Here is what your Events.cs code file should look like.
namespace MDICaseStudyPurchasing
{
public delegate void PurchaseOrderChangingEventHandler(object sender,
PurchaseOrderChangingEventArgs e);
public delegate void PurchaseOrderChangedEventHandler(object sender,
PurchaseOrderChangedEventArgs e);
}
This will show errors for the event arguments classes, let's make those now. Make another code file, name it EventArgs.cs. Again you need to add your namespace block, and a using statement for System. The event args classes will extend the System.EventArgs
class. Here is what your EventArgs code file should look like.
using System;
namespace MDICaseStudyPurchasing
{
public class PurchaseOrderChangingEventArgs : EventArgs
{
public PurchaseOrderChangingEventArgs() { }
}
public class PurchaseOrderChangedEventArgs : EventArgs
{
public PurchaseOrderChangedEventArgs() { }
}
}
Next, in our PurchaseOrder
class, we add the events, and their corresponding OnEvent methods.
....
[field: NonSerialized]
public event PurchaseOrderChangingEventHandler Changing;
[field: NonSerialized]
public event PurchaseOrderChangedEventHandler Changed;
....
private void OnChanging(PurchaseOrderChangingEventArgs e)
{
if (Changing != null) Changing(this, e);
}
private void OnChanged(PurchaseOrderChangedEventArgs e)
{
if (Changed != null) Changed(this, e);
}
Note the tag we've added to the event,
[field: NonSerialized]
. Because eventually we will be serializing the PurchaseOrder
objects for saving purposes, we want to make sure the events are ignored. If we were to try and serialize this class without excluding the events, the serialization process will try and serialize the event handler owners, in our case the owning form. You COULD mark the form class [Serializable]
but theres no need and it will only increase the file size of our saves. For normal fields you would only use the [NonSerializable]
tag (or [XmlIgnore]
for XML serialization) but for events, we need the additional "field:"
notation. This tells the serializer not to look at the event itself but the underlying delegates attached to it.------------------
So, now any time a change is made within the PurchaseOrder
class, we will call the OnChanging
and OnChanged
methods. The reason we made a Changing
event as well as the Changed
, is because as we progress, we will want to take some actions BEFORE the actual changes manifest, so we call OnChanging
before making the changes, and then call OnChanged
after the changes are made. For now, our text box is storing its contents in the PurchaseOrderNumber
property, so anytime this value changes we want to trigger our events. So in the getter/setter, let's add calls to our OnChanging
and OnChanged
methods into the set block. Your PurchaseOrderNumber
getter/setter should look like
public String PurchaseOrderNumber
{
get { return _purchaseOrderNumber; }
set
{
OnChanging(new PurchaseOrderChangingEventArgs());
_purchaseOrderNumber = value;
OnChanged(new PurchaseOrderChangedEventArgs());
}
}
Now let's subscribe to these events in our PurchaseOrderForm
. When initializing the instance of our enclosed PurchaseOrder
, let's also add event handlers for these two events. Inside your PurchaseOrderForm
constructor, let's add these event handlers by adding these two lines below the _purchaseOrder = new PurchaseOrder(); line
_purchaseOrder = new PurchaseOrder();
.....
_purchaseOrder.Changing += new PurchaseOrderChangingEventHandler(purchaseOrder_Changing);
_purchaseOrder.Changed += new PurchaseOrderChangedEventHandler(purchaseOrder_Changed);
Also we have an overloaded constructor, we need to add these handlers in every constructor. We also have a getter/setter method for our PurchaseOrder
instance, so in the setter block, we also need to add the event handlers, like so
public PurchaseOrder PurchaseOrder
{
get { return _purchaseOrder; }
set
{
_purchaseOrder = value;
_purchaseOrder.Changing += purchaseOrder_Changing;
_purcahseOrder.Changed += purchaseOrder_Changed;
}
}
Visual Studio IntelliType will auto create the handlers for you as you type, but for reference, here is what they should look like
private void purchaseOrder_Changin(object sender, PurchaseOrderChangingEventArgs e)
{
}
private void purchaseOrder_Changed(object sender, PurchaseOrderChangedEventArgs e)
{
}
For now lets leave them empty, we will come back to them shortly. We are also going to create a Changing
and Changed
event in our PurchaseOrderForm
class, and fire it each time the corresponding PurchaseOrder
event is fired. Essentially we are "bubbling up" the event through the PurchaseOrderForm
class. In the PurchaseOrderForm
class, add the two events, and their "OnEvent" methods.
public event PurchaseOrderChangingEventHandler Changing;
public event PurchaseOrderChangedEventHandler Changed;
.....
private void OnChanging(PurchaseOrderChangingEventArgs e)
{
if (Changing != null) Changing(this, e);
}
private void OnChanged(PurchaseOrderChangedEventArgs e)
{
if (Changed != null) Changed(this, e);
}
Now lets "bubble up" the events. Go back to the purchaseOrder_Changing
and purchaseOrder_Changed
event handlers, and call the corresponding "OnChanging" and "OnChanged" methods, passing the event args instance along
private void purchaseOrder_Changing(object sender, PurchaseOrderChangingEventArgs e)
{
OnChanging(e);
}
private void purchaseOrder_Changed(object sender, PurchaseOrderChangedEventArgs e)
{
OnChanged(e);
}
Now we can subscribe to the Changing
and Changed
events of the PurchaseOrderForm
instance of each opened form, and use them to control our save buttons. Lets go to our MDIForm
class now, and set these event handlers up. We currently have 2 sources for creating form instances, the ShowNewForm
method, and the OpenFile
method. In both of these, we want to add the event handlers for Changing
and Changed
, like so
private void ShowNewForm(object sender, EventArgs e)
{
PurchaseOrderForm newForm = new PurchaseOrderForm();
newForm.Changing += purchaseOrderForm_Changing;
newForm.Changed += purchaseOrderForm_Changed;
.....
}
private void OpenFile(object sender, EventArgs e)
{
.....
purchaseOrderForm = (PurchaseOrderForm)serializer.Deserialize(stream);
purchaseOrderForm.Changing += purchaseOrderForm_Changing;
purchaseOrderForm.Changed += purchaseOrderForm_Changed;
}
No need to write seperate handlers in both cases, only one set are needed, just use the same 2 handlers in both methods. Now each time an underlying PurchaseOrder
instance changes, the events will fire and "bubble up" to the parent form, where we can handle them. Before we write the handlers, let's add a boolean to PurchaseOrderForm
to keep up with whether the form has been saved. Add a public getter/setter for it as well
private boolean _saved;
public boolean Saved
{
get { return _saved; }
set { _saved = value; }
}
In the default constructor, lets set _saved
to false
. In the constructor that takes a PurchaseOrder
argument, however, this is used for opening existing documents, and since we will open a file, it doesn't need saving until a change is made, so set _saved
to true
here.
public PurchaseOrderForm()
{
_saved = false;
.....
}
public PurchaseOrderForm(PurchaseOrder purchaseOrder)
{
_saved = true;
.....
}
We need to also set this flag to false in the Changed
handler for PurchaseOrder
private void purchaseOrder_Changed(object sender, PurchaseOrderChangedEventArgs e)
{
_saved = false;
OnChanged(e);
}
And here is what the Changing
and Changed
handlers should look like for PurchaseOrderForm
private void purchaseOrderForm_Changing(object sender, PurchaseOrderChangingEventArgs e)
{
}
private void purchaseOrderForm_Changed(object sender, PurchaseOrderChangedEventArgs e)
{
PurchaseOrderForm activeChildForm = this.ActiveMdiChild as PurchaseOrderForm;
PurchaseOrderForm sendingForm = sender as PurchaseOrderForm;
if(activeChildForm == sendingForm)
{
saveToolStripButton.Enabled = saveToolStripMenuItem.Enabled = true;
}
}
In the above, in case a background change is made to the PurchaseOrder
while the associated form is NOT the active form, we don't want our UI buttons to change, so we compare the Active form to the event sending form, and only if they match, do we handle our UI.
Now we need to make a minor change to our SaveAs
method in PurchaseOrderForm
. Since we've added events to the type we are serializing, BinaryFormatter
will try to serialize the current instance's handlers, which will throw some errors because our PurchaseOrderForm
class is not marked Serializable
. So in our SaveAs
method, just before we serialize we need to detach the event handlers. Then after serializing we need to reattach them. Inside the using block, make these changes
BinaryFormatter serializer = new BinaryFormatter();
// Detach event handlers before serialization
_purchaseOrder.Changing -= purchaseOrder_Changing;
_purchaseOrder.Changed -= purchaseOrder_Changed;
// Serialize
serializer.Serialize(stream, _purchaseOrder);
// Attach event handlers back after serialization
_purchaseOrder.Changing += purchaseOrder_Changing;
_purchaseOrder.Changed += purchaseOrder_Changed;
stream.Close();
So at this point you could do a bit of testing, but the UI is still a bit clunky, because the Save buttons are still always enabled, which is not good because it can cause errors if there are no documents open and the buttons are clicked. Lets take a minute to fix this, and make our UI function more as it's intended. First, in the MDIForm
design view, lets go ahead and set the Save, Save As, Print, and Print Preview menu buttons, and the Save, Print, and Print Preview tool strip buttons Enabled
to false. Now when you first open the application, these buttons will show as disabled. Now we need to handle enabling these buttons at the proper times. MDI forms have an event will work just perfectly, MdiChildActivate
. This event will fire each time a child form is opened, closed, or switched to. Lets handler our buttons there, so override the OnMdiChildActive
method of MDIForm
. Since this is called on close as well, the first thing we will do is set every button to disabled. Then check the activated form to see which buttons need to be enabled.
protected overrid void OnMdiChildActivate(EventArgs e)
{
saveToolStripMenuItem.Enabled = saveToolStripButton.Enabled = saveAsToolStripMenuItem.Enabled = false;
printToolStripMenuItem.Enabled = printToolStripButton.Enabled = false;
printPreviewToolStripMenuItem.Enabled = printPreviewToolStripButton.Enabled = false;
PurchaseOrderForm activeChildForm = (PurchaseOrderForm)this.ActiveMdiChild;
if (activeChildForm != null)
{
printToolStripMenuItem.Enabled = printToolStripButton.Enable = true;
printPreviewToolStripMenuItem.Enabled = printPreviewToolStripButton.Enabled = true;
saveAsToolStripMenuItem.Enabled = true;
saveToolStripMenuItem.Enabled = saveToolStripButton.Enabled = !activeChildForm.Saved;
}
base.OnMdiChildActivate(e);
}
This concludes Part IV, and gives you a brief overview of how to create custom events in your objects, and handle these events in your UI. As we progress we will be adding more events, and using them to interact with our UI to present a usable interface to the user. I've had some requests for screen shots, so I'm going to spend some time updating this and the previous parts with a few screen shots, and then move on to Part V. In Part V we will be expanding our Document form, PurchaseOrderForm, which will involve creating custom UserControls.
Points of Interest
- Custom events
- Custom event arguments
- Responding to events using event handlers