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

MDI Application Case Study - Part III - Child Forms

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
20 Nov 2015CPOL12 min read 18.8K   596   9   5

Introduction

In Part III the focus will be our child form, PurchaseOrderForm. Again we will start simple, getting the basics down, and then we'll begin expanding from there.

Part III - Child Form

PurchaseOrderForm

In your project, create a new Windows Form and name it PurchaseOrderForm.cs. This will be our document form, and each instance will be a child form in our MDIForm. In a later installment we will begin constructing our document form to make it more of what we want, but for now, lets just add a text box to it so we can do something with it, and have some distinguishing detail between instances. From the toolbox, drag a RichTextBox onto the form, name it textBox, and size it to the autosnap margins of the form. This will give a several pixel margin around the text box. We want this to remain the same regardless of the form's sizing, so lets set the Anchor property of textBox to all four, Left Top Right and Bottom. Now the text box will auto size as the size of the form changes. Now we have something we can do on our document for the time being, let's leave it there for now.

Screenshot 1

Go back to MDIForm. Let's get our new document form functional as a child form. Creating MDIForm should have already created a series of methods. Find the ShowNewForm() method. It's already set up to open a new base form. Let's modify this to open a new instance of our document. Change this lines

C#
Form childForm = new Form();

....

childForm.Text = "Form " + childFormNumber++;

To

C#
PurchaseOrderForm childForm = new PurchaseOrderForm();

....

childForm.Text = "New Purchase Order " + childFormNumber++;

Now take a minute and try it out. Create a few new documents, see how the parent form behaves. Test out the Window commands for arranging the documents. Already we've created alot of application in very little time.

Document Processing - Saving - Serialization

Documents wouldn't be of much use if you couldn't save them. So let's work on saving our form. Theres a plethora of approaches to saving. At this point we could simply save a text file with the contents of the text box and be done. However, as we progress, our document is going to grow to contain alot of different types of information. You could still save a text file, but it would get increasingly complicated as you would have to constantly update your save procedure and open procedure every time your object model changes. Here is where serialization shines. With serialization we can simply serialize our object to a file stream, and deserialize it when we want to open it. We will touch on two different methods of serialization, XML Serialization, and Binary Serialization. Moving forward we will be using the Binary method.

First, before we begin serialization, lets prepare our form for saving. We will not actually be serializing the form itself, rather, we will serialize an instance of our PurchaseOrder class, which our form will contain. So first, if you started your own project you may want to download the source from Part I. Or you can go back and review Part I and construct your own ObjectBase to suit your own needs. We will continue assuming you have the classes from MDICaseStudyPurchasing.ObjectBase

First, in our PurchaseOrderForm class, add a using statement for MDICaseStudyPurchasing.ObjectBase

C#
using MDICaseStudyPurchasing.ObjectBase;

Next lets add a private PurchaseOrder, and a public getter/setter for it in our PurchaseOrderForm class

C#
private PurchaseOrder _purchaseOrder;

....

public PurchaseOrder PurchaseOrder
{
     get { return _purchaseOrder; }
     set { _purchaseOrder = value; }
}

In the default constructor let's initialize _purchaseOrder by adding at the top

C#
_purchaseOrder = new PurchaseOrder();

Also, lets add an overloaded constructor that takes a PurchaseOrder argument to create a new PurchaseOrderForm

C#
public PurchaseOrderForm(PurchaseOrder purchaseOrder)
{
    _purchaseOrder = purchaseOrder;
    InitializeComponent();
    textBox.Text = purchaseOrder.PurchaseOrderNumber;
}

The last line is temporary, for now we just want to save the contents of our test text box, so we will store it in the PurchaseOrderNumber of our underlying PurchaseOrder instance. Now when we serialize and save our form, we will be saving the contents of the text box.  Also, we want our data object, PurchaseOrder, to stay updated with any changes the user makes. As we get to Object Data Sources later, this will be much easier to keep up with. For now, lets just handle the TextChanged event of our text box, and use it to set the PurchaseOrderNumber of our underlying PurchaseOrder. In design view, TextChanged is the default event for RichTextBox, so simply double clicking the RichTextBox in design view will create the event handler for us. This is what the handler should look like

C#
private void textBox_TextChanged(object sender, EventArgs e)
{
    this.PurchaseOrder.PurchaseOrderNumber = textBox.Text;
}

Now the data object will stay updated to user changes. Before we can start serializing our PurchaseOrder instance, we need to prepare the PurchaseOrder class for serialization. To do this we just need to add the [Serializable] tag to the top of the class definition

C#
[Serializable]
public class PurchaseOrder
{

....

Additionally, any property to be serialized has to also be marked with [Serializable]. Since our PurchaseOrder contains our instances of our additional ObjectBase classes, open all the following classes and mark then [Serializable]

  • Item
  • ItemCollection
  • Address
  • BillingAddress
  • ShippingAddress
  • VendorAddress
  • Vendor
  • VendorCollection

XML Serialization also has a couple other requirements. You class must have a default constructor, and must have a public getter/setter for each property you want to serialize. If you have a public getter/setter that you don't want serialized you can tag it with [NonSerializable]. Now, in our PurchaseOrderForm class, lets make the following changes

Add a private String _fileName and a public getter/setter for it. This will keep up with the file name our document is saved as for quick saving.

C#
private String _fileName;

....

public String FileName
{
     get { return _fileName; }
     set { _fileName = value; }
}

We don't want to initialize FileName, because we want it to stay null until the first save, and then we will set it to the file name the user chooses when saving. So now our SaveAs(String file) method, this will actually do the serialization for us. To serialize into XML, we need to create a FileStream, then created a System.XML.Serialization.XMLSerializer with the type of PurchaseOrder. Then call XMLSerializer.Serialize(Stream, Object). Be sure to add a using statement for System.XML.Serialization

C#
using System.XML.Serialization;

....

public void SaveAs(String file)
{
    try
    {
        using (FileStream stream = File.Create(file))
        {
            XmlSerializer xmlSerializer = new XmlSerializer(typeof(PurchaseOrder));
            xmlSerializer.Serialize(stream, _purchaseOrder);
            stream.Close();
        }
        this.Text = file;
    }
    catch (IOException exception)
    {
        MessageBox.Show("Error saving file:\n\n" + exception.Message,
                        "File Save Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

If you aren't familiar with using statements, they keep the object in () in scope only within the { } of the statement, and dispose of the the object afterward. This is good to use anytime you are dealing with classes that access system resources. After serialization, we set the _fileName property to the provided file name, and also set the form's caption text to the provided file name. This helps the user keep up with which files they are modifying. Next our quick save method, Save(), will simply check to see if the _fileName is set, and if so it will use it to call SaveAs(String) again. 

C#
public void Save()
{
    if (_fileName != null)
    {
        SaveAs(_fileName);
    }
}

So now you know how to save using XML Serialization. However, going forward we will be using Binary Serialization, so lets cover how to achieve saving using this method. The only thing we need to change is the SaveAs(String) method. For reference I am going to leave the XML method in the source, renamed as SaveAsXML(String). First lets add a using statement for System.Runtime.Serialization.Formatters.Binary. This contains the BinaryFormatter we need for serialization

C#
using System.Runtime.Serialization.Formatters.Binary;

Next, in the SaveAs(String) method, change your using statement to the following

C#
using (FileStream stream = File.Create(file))
{
    BinaryFormatter serializer = new BinaryFormatter();
    serializer.Serialize(stream, _purchaseOrder);
    stream.Close();
}

Note - using serialization means that you can't serialize as Type A, and expect to deserialize as Type B. Any new properties you add to your class will be fine, but if you try to deserialize an old file, keep in mind that file didnt include the new properties, so the will initialize only as their default values.

So now we have our foundation for saving, so lets move back over to MDIForm, and handle the user interaction for saving. Again you have methods already created, so lets find the SaveAsToolStripMenuItem_Click method, which is the handler for the menu item File -> Save As. First, lets decide that our documents are always going to be the same file extension, lets use the extension ".pof". Lets change the Filter property of the SaveFileDialog that is already present in the method for us, change it to

C#
saveFileDialog.Filter = "Purchase Orders (*.pof) | *.pof";
saveFileDialog.DefaultExt = ".pof";
saveFileDialog.AddExtension = true;

DefaultExt and AddExtension will help ensure saving integrity, in case a user neglects to put the file extension at the end of the file name they are choosing, AddExtension set to true will cause it to be added to the filename if it isn't added by the user. DefaultExt is what tells the dialog what extension to add.

Now, in the if(showFileDialog.ShowDialog(this) == DialogResult.OK) clause, lets add the following lines

C#
PurchaseOrderForm currentForm = (PurchaseOrderForm)this.ActiveMdiChild;
currentForm.SaveAs(FileName);

Any time a new child form is selected, it becomes the ActiveMdiChild. However it is stored as a base Form, so we have to cast it to our PurchaseOrderForm before we can use it. So saving is now done, you can test the app, try it out, then go look in the directory and you'll see your .pof files. Now lets finish up saving by handling the quick save buttons. Again, in design view, since the Click event is the default event for buttons, you can simply double click the save button on the tool bar to create the event handler. To quick save, we want to get our current focused form, check to see if its FileName property is set, and if so call its Save() method. If not then we perform the same behavior as the SaveAs handler, prompting the user for a file location and name. The event handler should look like 

C#
private void saveToolStripButton_Click(object sender, EventArgs e)
{
    PurchaseOrderForm currentForm = (PurchaseOrderForm)this.ActiveMdiChild;
    if (currentForm.FileName != null)
    {
        currentForm.Save();
    }
    else
    {
        SaveFileDialog saveFileDialog = new SaveFileDialog();
        saveFileDialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
        saveFileDialog.Filter = "Purchase Orders (*.pof)|*.pof";
        if (saveFileDialog.ShowDialog(this) == DialogResult.OK)
        {
            String FileName = saveFileDialog.FileName;
            currentForm.FileName = FileName;
            currentForm.Text = FileName;
            currentForm.SaveAs(FileName);
        }
    }
}

Now lets do the same for the menu item File -> Save. Thats it for saving! In a later installment we will fine tune our UI so that the save buttons are only enabled when our document has changes, and warning the user to save if there are unsaved changes.

Document Processing - Opening - Deserialization

So opening, as you can imagine, is much the opposite of saving. If you went through the trouble of manually saving everything into a file, you'll have to manually parse the saved file to get your data back out. Again here is where serialization helps alot. Again find the already created method OpenFile() event handler and make the following changes

Again we only want to be concerned with our own documents, *.pof, so lets change the OpenFileDialog.Filter property to reflect this

C#
openFileDialog.Filter = "Purchase Orders (*.pof)|*.pof";

Now inside the if(openFileDialog(this) == DialogResult.OK) clause, add the lines

C#
try
{
    using (FileStream stream = File.Open(FileName, FileMode.Open))
    {
        XmlSerializer xmlSerializer = new XmlSerializer(typeof(PurchaseOrder));
        PurchaseOrder purchaseOrder = (PurchaseOrder)xmlSerializer.Deserialize(stream);

        PurchaseOrderForm purchaseOrderForm = new PurchaseOrderForm(purchaseOrder);
        purchaseOrderForm.FileName = FileName;
        purchaseOrderForm.MdiParent = this;
        purchaseOrderForm.Text = FileName;
        purchaseOrderForm.Show();
        stream.Close();
    }
}
catch (Exception exception)
{
    MessageBox.Show("Error with file:\n" + exception.Message,
                    "File Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

The only noteworthy thing here is, Deserialize() returns an Object, so it needs to be cast to the correct class.

Again since we are going to proceed with Binary Serialization, here is what we need to change. I am going to leave the XML portion commented out for reference in the source. Change the following lines

C#
XmlSerializer xmlSerializer = new XmlSerializer(typeof(PurchaseOrder));
PurchaseOrder purchaseOrder = (PurchaseOrder)xmlSerializer.Deserialize(stream);

To

C#
BinaryFormatter serializer = new BinaryFormatter();
PurchaseOrder purchaseOrder = (PurchaseOrder)serializer.Deserialize(stream);

Again, the BinaryFormatter retuns an Object, which needs to be cast for use. One last change, let's allow the user to open multiple files at once using the multi-select option of the OpenFileDialog. Instead of using FileName, we need to step through the FilesNames[] array, using each file name string to open a new form. This is what your new OpenFile() handler method should look like

C#
private void OpenFile(object sender, EventArgs e)
{
    OpenFileDialog openFileDialog = new OpenFileDialog();
    openFileDialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
    openFileDialog.Filter = "Purchase Orders (*.pof)|*.pof";
    openFileDialog.Multiselect = true;
    if (openFileDialog.ShowDialog(this) == DialogResult.OK)
    {
        foreach (String FileName in openFileDialog.FileNames)
        {
            try
            {
                using (FileStream stream = File.Open(FileName, FileMode.Open))
                {
                    BinaryFormatter serializer = new BinaryFormatter();
                    PurchaseOrder purchaseOrder = (PurchaseOrder)serializer.Deserialize(stream);

                    PurchaseOrderForm purchaseOrderForm = new PurchaseOrderForm(purchaseOrder);
                    purchaseOrderForm.FileName = FileName;
                    purchaseOrderForm.MdiParent = this;
                    purchaseOrderForm.Text = FileName;
                    purchaseOrderForm.Show();
                    stream.Close();
                }
            }
            catch (Exception exception)
            {
                MessageBox.Show("Error with file: " + FileName + "\n" + exception.Message,
                                "File Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }
}

That's it for opening. Now for the final section of Part III, printing.

Document Processing - Printing

For this section, we will deal with simple GDI based printing. In future installments we will change our print method from GDI to MigraDoc. MigraDoc will allow us to create one document, and render it either to a printer, or to a PDF file. For now lets deal with simply printing the text from our document. For simplicty sake, and to illustrate how to use the GDI printing, we will simply print the text of the form, 80 lines per page. First, double click on the Print tool strip button in the design interface for MDIForm. This will create your print button event handler. To use GDI printing you need System.Drawing.Printing. We will show a PrintDialog, and on printing we will create a PrintDocument, and place the settings from PrintDialog into the PrintDocument. This is how you allow the user to control which printer to use, and how many copies and so on. Here is what your print button event handler should look like

C#
private void printToolStripButton_Click(object sender, EventArgs e)
{
    PurchaseOrderForm currentForm = (PurchaseOrderForm)this.ActiveMdiChild;
    String documentName = currentForm.Text;
    PrintDialog printDialog = new PrintDialog();
    if (printDialog.ShowDialog(this) == System.Windows.Forms.DialogResult.OK)
    {
        PrintDocument printDoc = new PrintDocument();
        printDoc.PrinterSettings = printDialog.PrinterSettings;
        printDoc.DocumentName = documentName;
        printDoc.BeginPrint += printDoc_BeginPrint;
        printDoc.PrintPage += printDoc_PrintPage;
        printDoc.Print();
    }
}

PrintDocument.DocumentName is the property that control the title of the document displayed when the printing progress dialog is displayed. PrintDocument has two events that we want to handle, BeginPrint, and PrintPage. The BeginPrint event fires before printing begins, and gives you an opportunity to set up or take care of anything necessary before printing starts. The PrintPage event runs for each page printed. To alert the event that a next page is to be printed, we set the PrintPageEventArgs.HasMorePages property to true. For this example we will store the lines of the text box into a List<String> collection, and then render them from the collection. First lets use the BeginPrint event as an opportunity to build our List<String> collection for printing. Right above the BeginPrint event handler, declare a private List<String> named _printLines. Your BeginPrint event should look like this

C#
private List<String> _printLines;
private void printDoc_BeginPrint(object sender, PrintEventArgs e)
{
    PurchaseOrderForm currentForm = (PurchaseOrderForm)this.ActiveMdiChild;
    String printText = currentForm.PurchaseOrder.PurchaseOrderNumber;
    String[] lines = printText.Split('\n');
    _printLines = new List<String>(lines);
}

Now that we have our List<String> collection, all we need to do is step through it, and render each line to the printer. To do this, in the PrintPage event handler we will set an int called linesPerPage to 80, then use a while loop to remove a string and print it, until either linesPerPage or the end of the collection is reached. Then, if we have exceeded linesPerPage but there are more lines left to print, we simply set the HasMorePages flag to true, and the event will fire again for page 2. Here is what the PrintPage event handler should look like

C#
private void printDoc_PrintPage(object sender, PrintPageEventArgs e)
{
    int linesPerPage = 80;
    int printedLineCount = 0;
    float y = 25.0f;
    Font printFont = new Font("Times New Roman", 10.0f);

    while (_printLines.Count > 0 && printedLineCount < linesPerPage)
    {
        e.Graphics.DrawString(_printLines[0], printFont, Brushes.Black, 25.0f, y);
        _printLines.RemoveAt(0);
        printedLineCount++;
        y += 15.0f;
    }

    if (_printLines.Count > 0) e.HasMorePages = true;
}

GDI Printing is very similar to GDI painting, and for small printing tasks is very easy and straightforward to use.

That does it for Part III. In the next installment we will work on some of the UI aspects, and will start creating our own custom events, and responding to them.

Points of Interest

MDI Child Form
XML Serialization
Binary Serialization
GDI Printing

History

11/16/2015 - Updated PrintPage method to increment printedLineCount

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

 
GeneralMy vote of 4 Pin
HiDensity9-Dec-15 2:50
HiDensity9-Dec-15 2:50 
BugPrintPage Pin
mrtnu15-Nov-15 3:07
professionalmrtnu15-Nov-15 3:07 
GeneralRe: PrintPage Pin
stebo072816-Nov-15 3:43
stebo072816-Nov-15 3:43 
Generalsome screenshots will be very helpful Pin
Southmountain31-Oct-15 9:17
Southmountain31-Oct-15 9:17 
GeneralRe: some screenshots will be very helpful Pin
stebo072818-Nov-15 5:52
stebo072818-Nov-15 5:52 

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.