Document-View Architecture
Many developers will recognize document-view architecture as a staple of MFC design. In .NET, the emphasis is less critical because custom form classes can be equipped with most of the intelligence they need (as you saw in our refresh example), and don't require an additional separation between the document and the view. Tasks that typically required views, like scrolling, are dealt with effortlessly with the built-in smarts of most .NET controls.On the other hand, there are several scenarios that are particularly well suited to a dedicated document-view architecture:
When you are using complex documents.
When you are providing more than one view of the same document.
When you want the flexibility to provide different views in separate windows or in a single window.
When discussing MDI interfaces, a document is the actual underlying data. For example, with Microsoft Word the document is the memo, report, or resume the user is composing. The view is a window onto the document. For example, the view in Microsoft Word might just include the page that is currently being edited (which can be scrolled to different pages).A typical document-view application uses the following ingredients:
A document class.
A document view class that references an instance of a document.
An MDI child class that hosts the view.
An MDI container that holds all the MDI children.
Why would a document require more than one view? It's easy to think of a view as a window onto a different part of a document, but a view can also correspond to a representation of the document. For example, you could have an editing view where changes are made and a print preview that shows the final layout. Both views represent the same data in different ways and must be synchronized. However, they can't be cleanly dealt with in a single class. Similarly, you might have a document object that corresponds to a large amount of information from a database. You could simultaneously view this as a grid of records and as a diagram with two different views. Yet another example is an HTML file, which can be viewed as straight text or marked-up content.
A Document-View Ordering Program
Our next example presents a fairly sophisticated model that supports real-time previews using the document-view architecture. It includes the following ingredients:
An Order document object that contains a list of OrderItem objects.
Two view objects: OrderPrintPreview and OrderGridView. Both derive from the UserControl class, but they could be implemented just as easily using a Panel or some other control.
A Child form class, which can display either of the two view objects.
A main Parent class, which provides a toolbar and the event handling logic that creates the document objects and displays the child windows.
Resource classes, like Product, which represents an individual product, and PriceList, which provides a static GetItem() method that accepts a product ID and returns a Product object with product information.
Figure 10-7 shows the relationship of some of the classes in this example.

Figure 10-7: The document-view architecture in the ordering program
Document class
The heart of this application is the document class called Order, which represents a collection of items in a sales order. Because this is a fairly long piece of code, it helps to attack it piecemeal. The first ingredient is the Product class, which represents an item in the catalog.
public class Product
{
public string Name;
public string Description;
public decimal Price;
public Product(string name, string description, decimal price)
{
this.Name = name;
this.Description = description;
this.Price = price;
}
}
In an order, each product is identified solely by product ID. Here's the OrderItem class, which represents a line item in an order:
public class OrderItem
{
public int ID;
public OrderItem(int ID)
{
this.ID = ID;
}
}
Finally, there is the Order class, which contains a collection of OrderItem objects. The Order class is created as a custom collection by deriving from the CollectionBase class. This trick provides an added benefit to all clients, ensuring that they can easily iterate through the order items using foreach syntax. It also prevents deficient code from trying to add any objects other than OrderItem instances.Here's the basic framework for the Order class:
public class Order : CollectionBase
{
public event EventHandler DocumentChanged;
private string lastFilename = "[New Order]";
public string LastFileName
{
get
{
return lastFilename;
}
set
{
lastFilename = value;
}
}
public void Add(OrderItem item)
{
this.List.Add(item);
OnDocumentChanged(new EventArgs());
}
public void Remove(int index)
{
// Check to see if there is an item at the supplied index.
if (index > (this.Count - 1) || index < 0)
{
throw new System.IndexOutOfRangeException();
}
else
{
this.List.RemoveAt(index);
}
OnDocumentChanged(new EventArgs());
}
public OrderItem Item(int index)
{
return (OrderItem)this.List[index];
}
protected void OnDocumentChanged(System.EventArgs e)
{
// Note that this currently occurs as items are added or removed,
// but not when they are edited. To overcome this would require adding
// an additional OrderItem change event.
// Raise the DocumentChanged event.
if (DocumentChanged != null)
{
DocumentChanged(this, e);
}
}
}
The OnDocumentChanged() method is a critically important ingredient. This is the key that allows other views to update themselves when the list of items in the order is changed (either by adding a new item or removing an existing one).The Order class also includes two additional document-specific methods: Save and Open, which transfer the data to and from a file:
public void Open(string filename)
{
FileStream fs = new FileStream(filename, FileMode.Open);
StreamReader r = new StreamReader(fs);
do
{
this.Add(new OrderItem(int.Parse(r.ReadLine())));
} while (r.Peek() != -1);
r.Close();
fs.Close();
// By placing this last we ensure that the file will not be updated
// if a load error occurs.
this.LastFileName = filename;
}
public void Save(string filename)
{
FileStream fs = new FileStream(filename, FileMode.Create);
StreamWriter w = new StreamWriter(fs);
foreach (OrderItem item in this.List)
{
w.WriteLine(item.ID);
}
w.Close();
fs.Close();
// Note: a real pricing program would probably store the price in the file
// (required for orders) but update it to correspond with the current
// price for the item when the file is opened.
// By placing this last we ensure that the file will not be updated
// if a save error occurs.
this.LastFileName = filename;
}
All in all, the Order class is really built out of three parts: It contains data (the collection of OrderItem objects), it provides the functionality for saving and opening files, and it provides the DocumentChanged event that will prompt the appropriate views to update themselves when any changes are detected.
OrderGridView class
The OrderGridView presents a ListView that displays all the order items and provides support for adding and removing items. The view is created as a user control, which allows it to hold various combined controls and be tailored at design-time. The ListView is anchored so that it grows as the dimensions of the user control expand (see Figure 10-8).

Figure 10-8: The OrderGridView
public class OrderGridView : System.Windows.Forms.UserControl
{
// (Designer code omitted.)
public Order Document;
public OrderGridView(Order document)
{
// This is required to make sure the controls that were added at
// design-time are actually rendered.
InitializeComponent();
// Store a reference to the document, attach the event handler,
// and refresh the display.
this.Document = document;
this.Document.DocumentChanged += new EventHandler(RefreshList);
RefreshList(this, null);
}
private void RefreshList(object sender, System.EventArgs e)
{
// Update the ListView control with the new document contents.
if (list != null)
{
// For best performance, disable refreshes while updating the list.
list.SuspendLayout();
list.Items.Clear();
// Step through the list of items in the document.
Product itemProduct;
ListViewItem itemDisplay;
foreach (OrderItem item in this.Document)
{
itemDisplay = list.Items.Add(item.ID.ToString());
itemProduct = PriceList.GetItem(item.ID);
itemDisplay.SubItems.Add(itemProduct.Name);
itemDisplay.SubItems.Add(itemProduct.Price.ToString());
itemDisplay.SubItems.Add(itemProduct.Description);
}
list.ResumeLayout();
}
}
// Triggered when the Add button is clicked.
private void cmdAdd_Click(object sender, System.EventArgs e)
{
// Add a random item.
Random randomItem = new Random();
Document.Add(new OrderItem(randomItem.Next(1, 4)));
}
// Triggered when the Remove button is clicked.
private void cmdRemove_Click(object sender, System.EventArgs e)
{
// Remove the current item.
// The ListView is configured for single-selection only.
Document.Remove(list.SelectedIndices[0]);
}
}
Our simple example doesn't provide an additional product catalog-instead, a random order item is added every time the Add button is clicked. It also doesn't include any code for editing items. None of these details would change the overall model being used.You should also notice that the RefreshList() method handles the DocumentChanged event, ensuring that the list is rebuilt if any change is made by any view (or even through code).
OrderPrintPreview class
The OrderPrintPreview class is also a user control, but it only contains a single instance of the PrintPreview control. Once again, this example has been left intentionally crude. You could easily add other controls for zooming, moving from page to page, and otherwise configuring the print preview. Similarly, the printed output is very basic, and doesn't include details like an attractive title or letter-head. Figure 10-9 shows the OrderPrintPreview view in action.

Figure 10-9: The OrderPrintPreview view
The OrderPrintPreview class follows a similar design to the OrderGridView. A reference to the document is set in the constructor, and the RefreshList() method handles the DocumentChanged event. The only difference is that the RefreshList() needs to initiate printing using a PrintDocument instance. The PrintDocument.PrintPage event handler writes the output to the preview window.
public class OrderPrintPreview : System.Windows.Forms.UserControl
{
// (Designer code omitted.)
public Order Document;
private PrintDocument printDoc = new PrintDocument();
public OrderPrintPreview(Order document)
{
// This is required to make sure the controls that were added at
// design-time are actually rendered.
InitializeComponent();
// Store a reference to the document, attach the document event handlers,
// and refresh the display.
this.Document = document;
this.Document.DocumentChanged += new EventHandler(RefreshList);
this.printDoc.PrintPage += new PrintPageEventHandler(PrintDoc);
RefreshList(this, null);
}
private void RefreshList(object sender, System.EventArgs e)
{
// Setting this property starts the preview,
// even if the PrintDoc document is already assigned.
Preview.Document = printDoc;
}
// Tracks placement while printing.
private int itemNumber;
// The print font.
private Font printFont = new Font("Tahoma", 14, FontStyle.Bold);
private void PrintDoc(object sender,
System.Drawing.Printing.PrintPageEventArgs e)
{
// Tracks the line position on the page.
int y = 70;
// Step through the items and write them to the page.
OrderItem item;
Product itemProduct;
for (itemNumber == itemNumber; itemNumber < Document.Count; itemNumber++)
{
item = Document.Item(itemNumber);
e.Graphics.DrawString(item.ID.ToString(), printFont,
Brushes.Black, 70, y);
itemProduct = PriceList.GetItem(item.ID);
e.Graphics.DrawString(itemProduct.Name, printFont,
Brushes.Black, 120, y);
e.Graphics.DrawString(itemProduct.Price.ToString(), printFont,
Brushes.Black, 350, y);
// Check if more pages are required.
if ((y + 30) > e.MarginBounds.Height &&
itemNumber < (Document.Count - 1))
{
e.HasMorePages = true;
return;
}
// Move to the next line.
y += 20;
}
// Printing is finished.
e.HasMorePages = false;
itemNumber = 0;
}
}
Tip
Printing operations are threaded asynchronously, which allows you to code lengthy RefreshList() code without worrying. However, if you create other views that need to perform time-consuming work in their automatic refresh routines (like analyzing statistical data), you should perform the work on a separate thread, and callback at the end to display the final results. Chapter 7 shows an example of this technique with the BitmapViewer custom control.Child form class
So far, everything is designed according to the document-view ideal. Most of the data manipulation logic is concentrated in the Order class, while most of the presentation logic is encapsulated in the view classes. All that's left for the child form is to create the appropriate view and display it. This is implemented by adding an additional constructor to the form class that accepts an Order document object.
public class Child : System.Windows.Forms.Form
{
// (Designer code omitted.)
public enum ViewType
{
ItemGrid,
PrintPreview
}
public Order Document;
public Child(Order doc, ViewType viewType)
{
// This is required to make sure the controls that were added at
// design-time are actually rendered.
InitializeComponent();
// Configure the title.
this.Text = doc.LastFileName;
this.Document = doc;
// Create a reference for the view.
// This reference can accommodate any type of control.
Control view = null;
// Instantiate the appropriate view.
switch (viewType)
{
case ViewType.ItemGrid:
view = new OrderGridView(doc);
break;
case ViewType.PrintPreview:
view = new OrderPrintPreview(doc);
break;
}
// Add the view to the form.
view.Dock = DockStyle.Fill;
this.Controls.Add(view);
}
}
One advantage to this design is that you could easily create a child window that hosts a combination of views (for example, grid views for two different orders, or a grid view and print preview for the same document). This could even provide the flexibility to change the interface to an SDI style.
The Parent form class
The MDI parent provides a toolbar with basic options, and the typical event handling logic that allows users to open, close, and save documents. This code follows true "switchboard" style, and relies heavily on the other classes to actually perform the work.
public class Parent : System.Windows.Forms.Form
{
//(Designer code omitted.)
private string lastDir = "C:\\Temp";
private void toolBar1_ButtonClick(object sender,
System.Windows.Forms.ToolBarButtonClickEventArgs e)
{
if (e.Button == cmdOpen)
{
OpenFileDialog dlgOpen = new OpenFileDialog();
dlgOpen.InitialDirectory = lastDir;
dlgOpen.Filter = "Order Files (*.ord)|*.ord";
// Show the open dialog.
if (dlgOpen.ShowDialog() == DialogResult.OK)
{
Order doc = new Order();
try
{
doc.Open(dlgOpen.FileName);
}
catch (Exception err)
{
// All exceptions bubble up to this level.
MessageBox.Show(err.ToString());
return;
}
// Create the child form for the selected file.
Child frmChild = new Child(doc, Child.ViewType.ItemGrid);
frmChild.MdiParent = this;
frmChild.Show();
}
}
else if (e.Button == cmdNew)
{
// Create a new order.
Order doc = new Order();
Child frmChild = new Child(doc, Child.ViewType.ItemGrid);
frmChild.MdiParent = this;
frmChild.Show();
}
else if (e.Button == cmdSave)
{
// Save the current order.
if (this.ActiveMdiChild != null)
{
SaveFileDialog dlgSave = new SaveFileDialog();
Order doc = ((Child)this.ActiveMdiChild).Document;
dlgSave.FileName = doc.LastFileName;
dlgSave.Filter = "Order Files (*.ord)|*.ord";
if (dlgSave.ShowDialog() == DialogResult.OK)
{
try
{
doc.Save(dlgSave.FileName);
this.ActiveMdiChild.Text = dlgSave.FileName;
}
catch (Exception err)
{
// All exceptions bubble up to this level.
MessageBox.Show(err.ToString());
return;
}
}
}
}
else if (e.Button == cmdClose)
{
if (this.ActiveMdiChild != null)
{
this.ActiveMdiChild.Close();
}
}
else if (e.Button == cmdPreview)
{
// Launch a print preview child for the active order.
if (this.ActiveMdiChild != null)
{
Order doc = ((Child)this.ActiveMdiChild).Document;
Child frmChild = new Child(doc, Child.ViewType.PrintPreview);
frmChild.MdiParent = this;
frmChild.Show();
}
}
}
}
One interesting detail is the event handling code for the preview button. It determines whether there is a current document, and if there is, it opens a preview window with the same underlying document object.Figure 10-10 shows the finished application with its synchronized views. You can peruse the full code in the DocumentView project included with the samples for this chapter.

Figure 10-10: Synchronized views on the same document