DekGenius.com
[ Team LiB ] Previous Section Next Section

8.3 Windows Forms Development

The Form class in the System.Windows.Forms namespace represents a standard window that contains Windows controls. In this section, we walk you through the development of a Windows Forms application and introduce you to the rich set of Windows controls that can be used on a Windows Form.

8.3.1 Windows Forms Application

All Windows Forms applications start out with a derived class from the System.Windows.Forms.Form class. A simple Windows Forms application looks like the following:

public class MyForm : System.Windows.Forms.Form
{
  public MyForm(  )
  {
    Text = "Hello World";
  }
  public static void Main(  )
  {
    System.Windows.Forms.Application.Run(new MyForm(  ));
  }
}

Basically, you define a class MyForm, which derives from the System.Windows.Forms.Form class. In the constructor of MyForm class, you set the Text property of the Form to Hello World. That's all there is to it. The static Main function is the entry point to all applications. In the entry-point function, you call the static method Application.Run, which starts the message loop for the application. Because you also pass a form-derived object MyForm to the Run method, what we have is a Windows Forms application.

You can also include references to the namespaces to avoid typing the fully qualified name of classes such as System.Windows.Forms.Form or System.Windows.Forms.Application. To do this, include the following line at the beginning of the source file and omit the System.Windows.Forms prefix to your class names:

using System.Windows.Forms;

To build the previously listed application, we use the command-line C# compiler. Notice that the target type is an executable, not a DLL, as when we compiled our web service PubsWS (type this command all on one line):[3]

[3] You can also compile the simple file with csc MyForm.cs but it's better to know how to specify the target type and the references that your source relies on.

csc /t:winexe
    /r:system.dll
    /r:System.Windows.Forms.dll
    MyForm.cs

The standard Form object that is shown on the screen doesn't do much; however, it demonstrates the simplicity of creating a Windows Forms application. You can exit the application by clicking on the Close button of the Control Box on the titlebar of the form. When you do this, a quit message is injected into the message loop, and, by default, it is processed and the Application instance will stop.

8.3.2 Windows Controls

Windows Forms applications can be much more involved than the application shown earlier; however, the underlying concepts are the same. In this section, we introduce you to the rich set of Windows controls that you can use on your form, as well as data binding to some of these controls. We also show how event handling works in Windows Forms applications.

8.3.2.1 Adding controls onto the form

First of all, we create and add the control to the Controls collection of the form:

Button btn1 = new Button(  );
btn1.Text = "Click Me";
this.Controls.Add(btn1);

Adding other types of controls follows the same convention. There are three basic steps:

  1. Create the control.

  2. Set up the control's properties.

  3. Add the control to the Controls collection of the Form object.

8.3.2.2 Binding the event handler

This is swell, but what does the application do when you click on the button? Nothing. We have not yet bound the event handler to the button's event. To do that, we first have to create the event handler. An event handler is nothing more than a normal function, but it always has two parameters: object and EventArgs. The object parameter is filled with event originator. For example, if you clicked on a button on a form, causing the Click event to fire, the object parameter to the event handler will point to the button object that you actually clicked on. The EventArgs object represents the event itself. Using the same example, the EventArgs parameter will be the Click event with event arguments, such as the coordinates of the mouse, which button got clicked and so on. The following code excerpt shows the event handler for the Click event on a button:

void btn1_onclick(Object sender, EventArgs e)
{
  Text = "Sender: " + sender.ToString(  ) + " - Event: " + e.ToString(  );
}

That event handler changes the title of the form each time the button is clicked. Now that we have created the event handler, we assign it to the event Click of the button:

btn1.Click += new EventHandler(btn1_onclick);

That line of code constructs an EventHandler object from the method we passed in and passes the newly created object to the Click event of the button. We basically register a callback function when Click happens. (You may want to review Chapter 2 where we discuss delegates.) Here is the complete example:

using System;
using System.Windows.Forms;

public class MyForm : Form
{

  void btn1_onclick(object sender, EventArgs e)
  {
    Text = "Sender: " + sender.ToString(  ) + 
           " - Event: " + e.ToString(  );
  }

  public MyForm(  )
  {
    Text = "Hello World";

    Button btn1 = new Button(  );
    btn1.Text = "Click Me";
    this.Controls.Add(btn1);

    btn1.Click += new EventHandler(btn1_onclick);
  }

  public static void Main(  )
  {
    Application.Run(new MyForm(  ));
  }

}

When the user clicks on the button, our event handler is called because we've already registered for the click event. It is possible to add more than one event handler to a single event by repeating the assignment line for other event handlers. All handlers that are registered to handle the event are executed in the order in which they're registered. For example, we add the following function to the code:

void btn1_onclick2(object sender, EventArgs e)
{
  MessageBox.Show(String.Format("Sender: {0} - Event: {1}", 
                  sender.ToString(), e.ToString(  )));
}

and one more line to associate this function as an event handler in the case button btn1 is clicked:

    btn1.Click += new EventHandler(btn1_onclick);
    btn1.Click += new EventHandler(btn1_onclick2);

The result is as expected. Both event handlers get called.

You can also easily remove the event handler. Replace += with -=:

btn1.Click -= new EventHandler(btn1_onclick);

Binding event handlers to events at runtime provides the developer with unlimited flexibility. You can programmatically bind different event handlers to a control based on the state of the application. For example, a button click can be bound to the update function when the data row exists or to the insert function when it's a new row.

As you can see, the process of binding event handlers to events is the same in Windows Forms as in Web Forms. This consistency of programming model is possibly due their shared substrate, the CLR in both environments.

8.3.2.3 Data binding

There are two kinds of data binding in Windows Forms. The first involves simple Windows controls such as Label, TextBox, and Button. These simple controls can be bound to a single value only. The second involves Windows controls that can manage lists of data such as ListBox, ComboBox, and DataGrid. These list controls are bound to lists of values.

Let's look at the first type of data binding. In the following example, we bind text boxes to fields in a table from the Pubs database. We extend the simple Hello, World Windows Form application to include data access and data binding.

The first thing is to obtain the data from the database. (It's a good time to review ADO.NET in Chapter 5 if you did not read the book in the order presented.) Let's take a look at the following example of a C# file:

using System;
using System.Windows.Forms;
using System.Data;
using System.Data.OleDb;

public class MyForm : Form
{
  public static void Main(  )
  {
    Application.Run(new MyForm(  ));
  }

  private TextBox m_txtFirstName, m_txtLastName, m_txtPhone;
  private Button m_btnPrev, m_btnNext;
  private CurrencyManager m_lm;
  private DataSet m_ds;

  public MyForm(  )
  {
    Text = "Simple Controls Data Binding";

    // Create the first name text box.
    m_txtFirstName = new TextBox(  );
    m_txtFirstName.Dock = DockStyle.Top;

    // Create the last name text box.
    m_txtLastName = new TextBox(  );
    m_txtLastName.Dock = DockStyle.Top;

    // Create the phone text box.
    m_txtPhone = new TextBox(  );
    m_txtPhone.Dock = DockStyle.Top;

    // Add both first name and last name to the panel1.
    Panel panel1 = new Panel(  );
    panel1.Dock = DockStyle.Left;
    panel1.Controls.Add(m_txtFirstName);
    panel1.Controls.Add(m_txtLastName);
    panel1.Controls.Add(m_txtPhone);
    // Add panel1 to the left of the form.
    this.Controls.Add(panel1);

    // Create the up button and bind click to event handler.
    m_btnPrev = new Button(  );
    m_btnPrev.Text = "Up";
    m_btnPrev.Dock = DockStyle.Top;
    m_btnPrev.Click += new EventHandler(btnPrev_onclick);

    // Create the down button and bind click to event handler.
    m_btnNext = new Button(  );
    m_btnNext.Text = "Down";
    m_btnNext.Dock = DockStyle.Top;
    m_btnNext.Click += new EventHandler(btnNext_onclick);

    // Add both the up and down buttons to panel2.
    Panel panel2 = new Panel(  );
    panel2.Dock = DockStyle.Right;
    panel2.Width = 50;
    panel2.Controls.Add(m_btnNext);
    panel2.Controls.Add(m_btnPrev);
    // Add panel2 to the right of the form.
    this.Controls.Add(panel2);

    // Fill the dataset with the authors table from Pubs database.
    m_ds = new DataSet(  );
    string oSQL = "select au_fname, au_lname, phone from authors";
    string oConnStr = 
      "provider=sqloledb;server=(local);database=pubs;Integrated Security=SSPI";
    OleDbDataAdapter oDA = new OleDbDataAdapter(oSQL, oConnStr);
    oDA.Fill(m_ds, "tbl");

    // Bind the Text property of last name text box to field au_lname.
    m_txtLastName.DataBindings.Add("Text", 
                                   m_ds.Tables["tbl"], 
                                   "au_lname");

    // Bind the Text property of first name text box to field au_fname.
    m_txtFirstName.DataBindings.Add("Text", 
                                    m_ds.Tables["tbl"], 
                                    "au_fname");

    // Bind the Text property of phone text box to field phone.
    m_txtPhone.DataBindings.Add("Text", 
                                m_ds.Tables["tbl"], 
                                "phone");

    // Obtain the list manager from the binding context.
    
    m_lm = (CurrencyManager)this.BindingContext[m_ds.Tables["tbl"]];

  }

  protected void btnNext_onclick(object sender, EventArgs e)
  {
    // Move the position of the list manager.
    m_lm.Position += 1;
  }
  protected void btnPrev_onclick(object sender, EventArgs e)
  {
    // Move the position of the list manager.
    m_lm.Position -= 1;
  }
}

UI controls derive from the Control class, and inherit the DataBindings property (which is of type ControlsBindingCollection). This DataBindings property contains a collection of Binding objects that is used to bind any property of the control to a field in the list data source.

To bind a simple control to a record in the data source, we can add a Binding object to the DataBindings collection for the control using the following syntax:

controlName.DataBindings.Add("Property", datasource, "columnname");

where controlName is name of the simple control that you want to perform the data binding. The Property item specifies the property of the simple control you want to be bound to the data in column columnname.

The C# source file shows how to bind the Text property of the TextBox control m_txtLastName to the au_lname column of Authors table of the DataSet m_ds, as well as m_txtFirstName and m_txtPhone to columns au_fname and phone.

To traverse the list in the data source, we will use the BindingManagerBase object. The following excerpt of code shows you how to get to the binding manager for the data source bound to the controls on the form. In this case, because the data is of list type, the binding manager returned from the BindingContext is a CurrencyManager:[4]

[4] If the data source returns only one data value, the BindingManagerBase actually points to an object of type PropertyManager. When the data source returns a list of data value, the type is CurrencyManager.

// Obtain the list manager from the binding context.
m_lm = (CurrencyManager)this.BindingContext[m_ds.Tables["tbl"]];

To demonstrate the use of BindingManagerBase to traverse the data source, we add two buttons onto the form, btnNext and btnPrev. We then bind the two buttons' click events to btnNext_onclick and btnPrev_onclick, respectively:

protected void btnNext_onclick(object sender, EventArgs e)
{
  m_lm.Position += 1;
}

protected void btnPrev_onclick(object sender, EventArgs e)
{
  m_lm.Position -= 1;
}

As you use BindingManagerBase to manage the position of the list—in this case, the current record in the Authors table—the TextBox controls will be updated with new values. Figure 8-3 illustrates the user interface for the simple controls data-binding example.

Figure 8-3. Simple controls data binding
figs/nfe3_0803.gif

Now let's take a look at the other type of data binding. In this example, we will bind the whole authors table to a DataGrid:

using System;
using System.Windows.Forms;
using System.Data;
using System.Data.OleDb;

public class MyForm : Form
{
  public static void Main(  )
  {
    Application.Run(new MyForm(  ));
  }

  private Button m_btn1;
  private TextBox m_txt1;
  private DataGrid m_dataGrid1;

  public MyForm(  )
  {
    Text = "Hello World";

    m_txt1 = new TextBox(  );
    m_txt1.Text = "select * from authors";
    m_txt1.Dock = DockStyle.Top;
    this.Controls.Add(m_txt1);

    m_btn1 = new Button(  );     
    m_btn1.Text = "Retrieve Data";
    m_btn1.Dock = DockStyle.Top;
    m_btn1.Click += new EventHandler(btn1_onclick);
    this.Controls.Add(m_btn1);

    m_dataGrid1 = new DataGrid(  );
    m_dataGrid1.Dock = DockStyle.Fill;
    this.Controls.Add(m_dataGrid1);

    this.AcceptButton = m_btn1;
  }

  protected void btn1_onclick(object sender, EventArgs e)
  {
    try {
      DataSet ds = new DataSet(  );
      string oConnStr = 
        "provider=sqloledb;server=(local);database=pubs;Integrated Security=SSPI";
      OleDbDataAdapter oDA = 
        new OleDbDataAdapter(m_txt1.Text, oConnStr);
      oDA.Fill(ds, "tbl");
      
      /* You can specify the table directly like this
       *
       *    m_dataGrid1.DataSource = ds.Tables["tbl"];
       *
       * or specify the datasource and the table separately 
       * like this:
       */
      m_dataGrid1.DataSource = ds;
      m_dataGrid1.DataMember = "tbl";

    } catch(Exception ex) {
      MessageBox.Show("An error has occured. " + ex.ToString(  ));
    }
  }
}

Data binding for controls of type List in Windows Forms is similar to that of Web Forms. However, you don't have to call the DataBind method of the control. All you have to do is set the DataSource property of the UI control to the data source. The data source then has to implement the IEnumerable or IListSource (or IList, which implements IEnumerable) interfaces. As it turns out, there are hundreds of classes that can be used as data source, including DataTable, DataView, DataSet, and all array or collection type of classes.

The process for DataGrid data binding is also simple: just set the DataSource property of the DataGrid object to the data source, and you're all set. We name the table tbl when we add it to DataSet with the data adapter's Fill( ) method; therefore, the following line of code just indexes into the collection of tables in the DataSet using the table name:

m_dataGrid1.DataSource = ds.Tables["tbl"];

If the data source contains more than one table, you will also have to set the DataMember property of the DataGrid to the name of the table you want the control to bind to:

m_dataGrid1.DataSource = ds;
m_dataGrid1.DataMember = "tbl";

The results of binding the two tables to the DataGrid are shown in Figure 8-4 and Figure 8-5.

Figure 8-4. Binding the authors table to the DataGrid
figs/nfe3_0804.gif
Figure 8-5. Binding the titles table to the DataGrid
figs/nfe3_0805.gif
8.3.2.4 Arranging controls

After adding controls onto the form and setting the event handlings and data bindings, you are fully functional. However, for the visual aspect of your application, you might want to change the layout of the controls on the form. You can do this by setting up physical locations of controls with respect to the container to which the controls belong,[5] or you can dock or anchor the controls inside the container.

[5] This is similar to VB programming. Controls initially have absolute positions on the form, but they can be programmatically moved and resized while the application is running.

Docking of a control is very simple. You can dock your control to the top, left, right, or bottom of the container. If you dock your control to the top or the bottom, the width of your control will span the whole container. On the same token, if you dock the control to the left or the right, its height will span the height of the container. You can also set the Dock property to DockStyle.Fill, which will adjust the control to fill the container.

The anchoring concept is a bit different. You can anchor your control inside your container by tying it to one or more sides of the container. The distance between the container and the control remains constant at the anchoring side.

You can also use a combination of these techniques by grouping controls into multiple panels and then organizing these panels on the form. With docking and anchoring, there is no need to programmatically calculate and reposition or resize controls on the form.

If you've ever done Java Swing development, you might notice that the current Microsoft .NET Windows Forms framework is similar to JFC with respect to laying out controls; however, it is missing the Layout Manager classes such as GridLayout and FlowLayout to help lay out controls in the containers. We hope that in future releases of the .NET SDK, some sort of layout manager will be included.[6] Currently, if you are writing your Windows Forms application using Visual Studio .NET, you will have more than enough control over the layout of controls on your form.

[6] Microsoft provides some interesting examples of how you can develop layout managers. The URL is http://msdn.microsoft.com/library/en-us/dndotnet/html/custlaywinforms.asp.

8.3.3 Visual Inheritance

Visual inheritance was never before possible on the Windows platform using Microsoft technologies. Prior to the release of Microsoft .NET (and we are only talking about VB development here), developers used VB templates to reuse a form. This is basically a fancy name for copy-and-paste programming. Each copy of a VB template can be modified to fit the current use. When the template itself is modified, copies or derivatives of the template are not updated. You either have to redo each one using copy-and-paste or just leave them alone.

With the advent of Microsoft .NET, where everything is now object-oriented, you can create derived classes by inheriting any base class. Since a form in Windows Forms application is nothing more than a derived class of the base Form class, you can actually derive from your Form class to create other Form classes.

This is extremely good for something like a wizard-based application, where each of the forms looks similar to the others. You can create the common look-and-feel form as your base class and then create each of the wizard forms by deriving from this base class.

8.3.4 MDI Applications

There are two main styles of user interfaces for Windows-based applications: Single Document Interface (SDI) and Multiple Document Interface (MDI).[7] For SDI applications, each instance of the application can have only one document. If you would like more than one open document, you must have multiple instances of the application running. MDI, on the other hand, allows multiple documents to be open at one time in one instance of the application. Another good thing about MDI application is that, depending of the type of document currently open, the main menu for the application changes to reflect the operations that you can perform on the document.

[7] Other styles are Explorer, Wizard, etc., but we are not going discuss all of them in this book.

While it is easy to implement both SDI and MDI applications using the Windows Forms architecture, we only show an example of MDI in this section.

MDI application architecture borrows the same pattern of Windows Forms architecture. Basically, you have one form acting as the container form and other forms acting as child forms.

The Form class provides a number of properties and methods to help in the development of MDI applications, including IsMdiContainer, IsMdiChild, MdiParent, MdiChildren, ActiveMdiChild, and LayoutMdi( ).

The first thing we want to show you is the bare minimum main form for our MDI application:

using System;
using System.Windows.Forms;

public class MdiMainForm : Form
{
  public MdiMainForm(  )
  {
    this.Text = "MDI App for Text and Images";
    // This is the MDI container.
    this.IsMdiContainer = true;
  }
  public static void Main(string[] args) 
  {
    Application.Run(new MdiMainForm(  ));
  }
}

Believe it or not, this is basically all you have to do for the main form of the MDI application! For each of the child forms that we will be spawning from this main form, we will set its MdiParent property to point to this main form.

In the following code excerpt, we load a child form of the main form:

 . . . 
  Form a = new Form(  );
  a.MdiParent = this;
  a.Show(  );

  Form b = new Form(  );
  b.MdiParent = this;
  b.Show(  );
 . . .

Again, all it takes to spawn a child form of the MDI application is a single property, MdiParent. In your application, you will replace the type for forms a and b with your own form classes. (As shown later in this chapter, we have ImageForm and TextForm.)

One other point that makes MDI applications interesting is the fact that there is one set of main menus and it is possible for child forms to merge their menus with the MDI frame. We also show you how to incorporate menus into our main MDI form, and later in this section, how the child form's menus are merged to this main menu.

The whole menu architecture in Windows Forms application revolves around two classes:[8] MainMenu and MenuItem. MainMenu represents the complete menu for the whole form. A MenuItem represents one menu item; however, each menu item contains child menu items in the MenuItems property. Again, you start to see the pattern of controls and containers here, too, although the menu classes are not controls.[9] For example, if we are to have two top-level menus (e.g., File and Window), then basically, we have to set up the MainMenu object so that it contains two menu items in its MenuItems property. We can do so using the Add method of the MenuItems property to insert menu items dynamically into the collection. If we know ahead of time the number of menu items, we can declaratively assign an array of menu items to this property. Recursively, we can have the File or the Window menu items contain a number of sub-menu items in their MenuItems property the same way we set up the main menu.

[8] The third class is ContextMenu, but we won't discuss it in the scope of this book.

[9] MainMenu, MenuItem and ContextMenu derive from Menu.

Let's take a look at the source code:

using System;
using System.Windows.Forms;

public class MdiMainForm : Form
{
  // Menu Items under File Menu
  private MenuItem mnuOpen, mnuClose, mnuExit;

  // Menu Items under the Window Menu
  private MenuItem mnuCascade, mnuTileHorz, mnuTileVert,
                   mnuSeparator, mnuCloseAll, mnuListMDI;

  // The File and Window Menus
  private MenuItem mnuFile, mnuWindow;

  // The Main Menu
  private MainMenu mnuMain;

  public MdiMainForm(  )
  {
    this.Text = "MDI App for Text and Images";

    // File Menu Item
    mnuFile = new MenuItem(  );
    mnuFile.Text = "&File";
    mnuFile.MergeOrder = 0;

    // Window Menu Item
    mnuWindow = new MenuItem(  );
    mnuWindow.MergeOrder = 2;
    mnuWindow.Text = "&Window";

    // Main Menu contains File and Window
    mnuMain = new MainMenu(  );
    mnuMain.MenuItems.AddRange( 
        new MenuItem[2] {mnuFile, mnuWindow});

    // Assign the main menu of the form.
    this.Menu = mnuMain;

    // Menu Items under File menu
    mnuOpen = new MenuItem(  );
    mnuOpen.Text = "Open";
    mnuOpen.Click += new EventHandler(this.OpenHandler);
    mnuClose = new MenuItem(  );
    mnuClose.Text = "Close";
    mnuClose.Click += new EventHandler(this.CloseHandler);
    mnuExit = new MenuItem(  );
    mnuExit.Text = "Exit";
    mnuExit.Click += new EventHandler(this.ExitHandler);
    mnuFile.MenuItems.AddRange(
        new MenuItem[3] {mnuOpen, mnuClose, mnuExit});

    // Menu Items under Window menu
    mnuCascade = new MenuItem(  );
    mnuCascade.Text = "Cascade";
    mnuCascade.Click += new EventHandler(this.CascadeHandler);
    mnuTileHorz = new MenuItem(  );
    mnuTileHorz.Text = "Tile Horizontal";
    mnuTileHorz.Click += new EventHandler(this.TileHorzHandler);
    mnuTileVert = new MenuItem(  );
    mnuTileVert.Text = "Tile Vertical";
    mnuTileVert.Click += new EventHandler(this.TileVertHandler);
    mnuSeparator = new MenuItem(  );
    mnuSeparator.Text = "-";
    mnuCloseAll = new MenuItem(  );
    mnuCloseAll.Text = "Close All";
    mnuCloseAll.Click += new EventHandler(this.CloseAllHandler);
    mnuListMDI = new MenuItem(  );
    mnuListMDI.Text = "Windows . . . ";
    mnuListMDI.MdiList = true;

    mnuWindow.MenuItems.AddRange(
        new MenuItem[6] {mnuCascade, mnuTileHorz, mnuTileVert,
                         mnuSeparator, mnuCloseAll, mnuListMDI});

    // This is the MDI container.
    this.IsMdiContainer = true;
  }
  public static void Main(string[] args) 
  {
    Application.Run(new MdiMainForm(  ));
  }
 . . . 
}

(Note that this source-code listing is completed in the event handlers listing that follows.)

We first declare all the menu items that we would like to have, along with one MainMenu instance in the class scope. In the main-application constructor, we then instantiate the menu items and set their Text properties. For the two top-level menu items, we also set the MergeOrder property so that we can control where the child forms will merge their menu to the main form menu. In this case, we've set up the File menu to be of order 0 and the Window menu to be of order 2. As you will see later, we will have the child menu's MergeOrder set to 1 so that it is between the File and Window menus.

We then add both the File and the Window menus to the main menu's MenuItems collection by using the AddRange( ) method:

 mnuMain.MenuItems.AddRange( 
        new MenuItem[2] {mnuFile, mnuWindow});

Note that at this time, the File and Window menus are still empty. We then assign mnuMain to the MainMenu property of the Form object. At this point, we should be able to see the File and Window menus on the main form; however, there is no drop-down yet.

Similar to how we create menu items and add them to the main menu's MenuItems collection, we add menu items into both the File and Window menu. However, one thing is different. We also bind event handlers to the Click events of the menu items. Let's take one example, the Open menu item:

mnuOpen = new MenuItem(  );
mnuOpen.Text = "Open";
mnuOpen.Click += new EventHandler(this.OpenHandler);

Note that the syntax for binding the event handler OpenHandler to the event Click of the MenuItem class is similar to any other event binding that we've seen so far. Of course, we will have to provide the function body in the MDI main class.

While we are talking about menus, another interesting piece of information is the mnuListMDI MenuItem at the end of the Window menu. We set the MdiList property of this MenuItem to true, as shown in the following code fragment, so that it will automatically show all the opened documents inside the MDI application.

mnuListMDI.Text = "Windows . . . ";
mnuListMDI.MdiList = true;

See Figure 8-6 for an example of how this feature shows up at runtime.

Figure 8-6. MdiList autogenerated menu entries
figs/nfe3_0806.gif

The following code is for the event handlers that we've set up for various menu items in this main form (this completes the MdiMainForm class listing):

protected void OpenHandler(object sender, EventArgs e)
{
  //MessageBox.Show("Open clicked");
  OpenFileDialog openFileDlg = new OpenFileDialog(  );
  if(openFileDlg.ShowDialog(  ) == DialogResult.OK)
  {
    try
    {
      String sFN = openFileDlg.FileName;
      String sExt = sFN.Substring(sFN.LastIndexOf("."));
      sExt = sExt.ToUpper(  );
      //MessageBox.Show(sFN + " " + sExt);
      if(sExt == ".BMP" || sExt == ".JPG" || sExt == ".GIF")
      {
        ImageForm imgForm = new ImageForm(  );
        imgForm.SetFileName(sFN);
        imgForm.MdiParent = this;
        imgForm.Show(  );
      }
      else if(sExt == ".TXT" || sExt == ".VB" || sExt == ".CS")
      {
        TextForm txtForm = new TextForm(  );
        txtForm.SetFileName(sFN);
        txtForm.MdiParent = this;
        txtForm.Show(  );
      }
      else
      {
        MessageBox.Show("File not supported.");
      }
    }
    catch(Exception ex)
    {
      MessageBox.Show ("Error: " + ex.ToString(  ));
    }
  }
}

protected void CloseHandler(object sender, EventArgs e)
{
   if(this.ActiveMdiChild != null)
  {
    this.ActiveMdiChild.Close(  );
  }
}

protected void ExitHandler(object sender, EventArgs e)
{
  this.Close(  );
}

protected void CascadeHandler(object sender, EventArgs e)
{
  this.LayoutMdi(MdiLayout.Cascade);
}

protected void TileHorzHandler(object sender, EventArgs e)
{
  this.LayoutMdi(MdiLayout.TileHorizontal);
}

protected void TileVertHandler(object sender, EventArgs e)
{
  this.LayoutMdi(MdiLayout.TileVertical);
}

protected void CloseAllHandler(object sender, EventArgs e)
{
  int iLength = MdiChildren.Length;
  for(int i=0; i<iLength; i++)
  {
    MdiChildren[0].Dispose(  );
  }
}

The functionality of the OpenHandler event handler is simple. We basically open a common file dialog box to allow the user to pick a file to open. For simplicity's sake, we will support three image formats (BMP, GIF, and JPG) and three text file extensions (TXT, CS, and VB). If the user picks the image-file format, we open the ImageForm as the child form of the MDI application. If a text-file format is selected instead, we use the TextForm class. We will show you the source for both the ImageForm and TextForm shortly.

To arrange the children forms, we use the LayoutMdi method of the Form class. This method accepts an enumeration of type MdiLayout. Possible values are Cascade, ArrangeIcons, TileHorizontal, and TileVertical.

The form also supports the ActiveMdiChild property to indicate the current active MDI child form. We use this piece of information to handle the File Close menu item to close the currently selected MDI child form.

To handle the CloseAll menu click event, we loop through the collection of all MDI child forms and dispose them all.

The following is the source for ImageForm class:

using System;
using System.Drawing;
using System.Windows.Forms;

public class ImageForm : System.Windows.Forms.Form
{
  private MenuItem mnuImageItem;
  private MenuItem mnuImage;
  private MainMenu mnuMain;
  private Bitmap m_bmp;

  public ImageForm(  )
  {

    mnuImageItem = new MenuItem(  );
    mnuImageItem.Text = "Image Manipulation";
    mnuImageItem.Click += new EventHandler(this.HandleImageItem);

    mnuImage = new MenuItem(  );
    mnuImage.Text = "&Image";
    mnuImage.MergeOrder = 1;    // Merge after File but before Window.
    mnuImage.MenuItems.AddRange(new MenuItem[1] {mnuImageItem});

    mnuMain = new MainMenu(  );
    mnuMain.MenuItems.AddRange( new MenuItem[1] {mnuImage});
    this.Menu = mnuMain;
  }

  public void SetFileName(String sImageName)
  {
    try
    {
      m_bmp = new Bitmap(sImageName);
      Invalidate(  );
      this.Text = "IMAGE: " + sImageName;
    }
    catch(Exception ex)
    {
      MessageBox.Show ("Error: " + ex.ToString(  ));
    }
  }

  protected override void OnPaint(PaintEventArgs e)
  {
    if(m_bmp != null)
    {
      Graphics g = e.Graphics;
      g.DrawImage(m_bmp, 0, 0, m_bmp.Width, m_bmp.Height);
    }
  }

  protected void HandleImageItem(object sender, EventArgs e)
  {
    MessageBox.Show("Handling the image.");
  }
}

Because this ImageForm class needs to draw the image file on the form, we include a reference to the System.Drawing namespace. To render the image file onto the form, we rely on the Bitmap and Graphics classes. First of all, we get the input filename and construct the Bitmap object with the content of the input file. Next, we invalidate the screen so that it will be redrawn. In the overriden OnPaint method, we obtained a pointer to the Graphics object and asked it to draw the Bitmap object on the screen.

One other point that we want to show you is the fact that the Image menu item has its MergeOrder property set to 1. We did this to demonstrate the menu-merging functionality of MDI applications. When this form is displayed, the main menu of the MDI application changes to File, Image, and Window.

To complete the example, following is the source to the TextForm class:

using System;
using System.Windows.Forms;
using System.IO;

public class TextForm : Form
{
  private MenuItem mnuTextItem;
  private MenuItem mnuText;
  private MainMenu mnuMain;
  private TextBox textBox1;

  public TextForm(  )
  {
    mnuTextItem = new MenuItem(  );
    mnuTextItem.Text = "Text Manipulation";
    mnuTextItem.Click += new EventHandler(this.HandleTextItem);

    mnuText = new MenuItem(  );
    mnuText.Text = "&Text";
    mnuText.MergeOrder = 1;    // Merge after File but before Window.
    mnuText.MenuItems.AddRange(new MenuItem[1] {mnuTextItem});

    mnuMain = new MainMenu(  );
    mnuMain.MenuItems.AddRange(new MenuItem[1] {mnuText});
    this.Menu = mnuMain;

    textBox1 = new TextBox(  );
    textBox1.Multiline = true;
    textBox1.Dock = System.Windows.Forms.DockStyle.Fill;
    this.Controls.Add (this.textBox1);
  }

  public void SetFileName(String sFileName)
  {
    StreamReader reader = File.OpenText(sFileName);
    textBox1.Text = reader.ReadToEnd(  );
    reader.Close(  );
    textBox1.SelectionLength = 0;
    this.Text = "TEXT: " + sFileName;
  }

  protected void HandleTextItem(object sender, EventArgs e)
  {
    MessageBox.Show("Handling the text file.");
  }

}

Similar to the ImageForm class, the TextForm class also has its menu inserted in the middle of File and Window. When a TextForm becomes the active MDI child form, the menu of the MDI application becomes File, Text, and Window. This menu-merging is done automatically. All we have to do is set up the MergeOrder properties of the menu items.

For the functionality of the TextForm, we have a simple TextBox object. We set its Multiline property to true to simulate a simple text editor and have its docking property set to fill the whole form. When the main form passes the text filename to this form, we read the input file and put the content into the text box.

Figure 8-7 illustrates the screen shot for this MDI application at runtime. In this instance, we have three TextForms and three ImageForms open concurrently.

Figure 8-7. The MDI application
figs/nfe3_0807.gif

The following script is used to build this MDI application. As you can see, the target parameter is set to winexe to indicate that the result of the compilation will be an executable instead of library, which would result in a DLL. Because we make use of the graphics package for our image rendering, we also have to add the reference to the System.drawing.dll assembly. We have three forms in this application: the main form, which is named MDIApp, and the two MDI child forms, ImageForm and TextForm (make sure you type these commands all on one line).[10]

[10] Again, you can compile the executable without specifying the target type or the references.

csc /t:winexe
    /r:System.Windows.Forms.dll 
    /r:system.drawing.dll 
    MDIApp.cs 
    ImageForm.cs 
    TextForm.cs

8.3.5 Stage Deployment

Imagine the MDI application from the previous example with hundreds of different types of files supported. For each of the file types, we'd probably have a class similar to the ImageForm and TextForm classes. Instead of compiling and linking all of these classes into your application, wouldn't it be nice if we could download and install a class on the fly when we use the appropriate file type? In this section, we show how you can convert the previous MDI application to allow just that.

Conceptually, we will have the main executable act as a controller. It will be the first assembly downloaded onto the client machine. Once the user chooses to open a particular file type, this controller determines which supporting DLLs should be downloaded to handle the file type, downloads and installs the DLL, and then asks the class in the downloaded assembly to display the selected file.

In order for the controller to communicate with all supporting classes, we abstract the commonality out of ImageForm and TextForm to create the abstract class BaseForm:[11]

[11] Because we will demonstrate running MDIApp.exe from a browser later, BaseForm.dll has to be explicitly flagged to grant MDIApp.exe usage via the AllowPartiallyTrustedCallers attribute. This is to accommodate the Version 1.1 security changes for Microsoft .NET Framework from RCs.

using System;
using System.Windows.Forms;
using System.Security;
[assembly:AllowPartiallyTrustedCallers]
namespace BaseForm
{
 abstract public class BaseForm : Form
 {
  abstract public void SetFileName(String sFileName);
 }
}

We will rewrite both ImageForm and TextForm to make them inherit from BaseForm. Any other form that you will need to write for other file types will have to also inherit from BaseForm. This way, all the controller module has to do is to know how to talk to BaseForm and everything should be fine.

The followings are the changes to ImageForm.cs source file. All we did was to wrap the class inside a namespace call ImageForm, make ImageForm class inherit from BaseForm, and providing the implementation for the abstract method SetFileName( ). The rest of the code is the same with the original ImageForm.cs.

//  . . . 

namespace ImageForm
{
  public class ImageForm : BaseForm.BaseForm
  {

    //  . . . 

    override public void SetFileName(String sImageName)
    {
      //  . . . 
    }

    //  . . . 

  }
}

We apply the same thing to TextForm.cs as we do for ImageForm.cs. The following summarizes the changes:

//  . . . 

namespace TextForm
{
  public class TextForm : BaseForm.BaseForm
  {

    //  . . . 
 
override public void SetFileName(String sFileName)
    {
      //  . . . 
    }

    //  . . . 

  }
}

Now, instead of having the MDIApp compiled and linked with ImageForm.cs and TextForm.cs and directly using ImageForm and TextForm class in OpenHandler( ) function, we utilize the reflection namespace to load each assembly from a predetermined URL on the fly. Replace the old code for handling images in OpenHandler( ):

ImageForm imgForm = new ImageForm(  );
imgForm.SetFileName(sFN);

with:

Assembly ass = Assembly.LoadFrom("http://localhost/MDIDLLS/ImageForm.dll");
BaseForm.BaseForm imgForm = 
          (BaseForm.BaseForm) ass.CreateInstance("ImageForm.ImageForm");
imgForm.SetFileName(sFN);

The three lines of code essentially do the following:

  1. Download the assembly from the specified URL if the client does not already have the latest version of the assembly.

  2. Create an instance of ImageForm class in ImageForm namespace and cast it to BaseForm.

  3. Instruct the BaseForm derived class to deal with the image file.

For the TextForm, replace the following old code for handling text files in OpenHandler( ):

TextForm txtForm = new TextForm(  );
txtForm.SetFileFile(sFN);

with:

Assembly ass = Assembly.LoadFrom("http://localhost/MDIDLLS/TextForm.dll");
BaseForm.BaseForm txtForm = 
          (BaseForm.BaseForm) ass.CreateInstance("TextForm.TextForm");
txtForm.SetFileName(sFN);

The last thing to do is to make MDIApp use the Reflection namespace by inserting the following line of code:

using System.Reflection;

You will have to compile BaseForm.cs, ImageForm.cs, and TextForm.cs into BaseForm.dll, ImageForm.dll, and TextForm.dll in that order because ImageForm and TextForm derive from BaseForm.

The following command lines will do the job:

csc /t:library /r:System.Windows.Forms.dll baseform.cs
csc /t:library /r:System.Windows.Forms.dll;baseform.dll imageform.cs
csc /t:library /r:System.Windows.Forms.dll;baseform.dll textform.cs

You will also have to compile the main module MDIApp.cs. Notice that the only thing the MDIApp.cs needs to know is BaseForm:

csc /t:winexe 
    /r:System.Windows.Forms.dll;system.drawing.dll;baseform.dll MDIApp.cs

Once you have all the components compiled, copy them all into a virtual directory call MDIDLLs because this is the predefined location where these components are to be downloaded.[12]

[12] Make sure this virtual directory execute permission does not have executable enabled. In other words, just create the virtual directory with the default settings.

One last step you will have to do is to use the .NET Framework Configuration tool to give file I/O permission to "adjust zone security" and give Local Intranet full trust level. The tool can be found at Administrative Tools/Microsoft .NET Framework Configuration. Choose to Configure Code Access Security and then Adjust Zone Security. Give Local Intranet full trust level for the duration of this experiment. It is not recommended that you use this setting without knowing all consequences; however, this is the simplest thing to do to quickly demonstrate the experiment. For your enterprise application, consult the .NET security documentation for further recommendations.

And now everything is set up. All you have to do is to point your browser to http://localhost/MDIDLLs/MDIApp.exe and see how the components are automatically downloaded and run.

Another utility you might want to use to inspect the global assembly cache (where the downloaded components reside) is gacutil. Use gacutil /ldl to list all downloaded assemblies and gacutil /cdl to clear the downloaded assemblies. When you first see the MDIApp main form, the only assembly downloaded is MDIApp. But as soon as you try to open an image file or a text file, ImageForm or TextForm along with BaseForm will be downloaded.

As you can see, with this setup, you won't need to have hundreds of DLLs when you only use a couple of file types. Another cool thing is that when the DLLs on the server are updated, you will automatically have the latest components.

Of course, you can make this MDIApp more production-like by having the main module accessing a Web Service that lists the file types this application supports and the class names as well as the assembly filenames needed to be downloaded. Once this is setup, you can have conditioning code based on the file type; you can pass the assembly filename URL to Assembly.LoadFrom( ) method; you can use the class name to create the class instance; all of these make the main module more generic. Suppose the Web Service that lists the supporting file types for this MDIApp reads information from a database or an XML file. When you need to introduce new file type and supporting assembly to deal with the new file type, all you have to do is add an entry to your database or your XML file and add the assembly file into the virtual directory. We think this is an exercise you should definitely try.

    [ Team LiB ] Previous Section Next Section