Click here to Skip to main content
15,867,488 members
Articles / Programming Languages / C#

MDI Case Study Purchasing - Part IV - Events

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
20 Nov 2015CPOL7 min read 8.9K   395   9  

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.

C#
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.

C#
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. 

C#
....
[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);
}
----- UPDATE -----

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

C#
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

C#
_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

C#
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

C#
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.

C#
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

C#
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

C#
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

C#
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.

C#
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

C#
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

C#
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

C#
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.

C#
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

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

 
-- There are no messages in this forum --