Implementing the Project


The XML editor application you will build in this chapter is an MDI application. The user can open any number of XML documents and get those documents validated. For each document, the user can also build a tree for the elements in the document. Each document encapsulates the StyledTextArea control discussed in Chapter 1, "Creating a Custom Control: StyledTextArea".

The MDI container uses the following components:

  • XmlViewer

  • TextPrinter

  • Document

In turn the Document component uses the FindDialog component. The following sections discuss each of these components before dissecting the MDI container.

Using the XmlViewer Component

The XmlViewer component displays the tree representing an XML document's hierarchical data structure. Figure 2-13 shows an example of an XML viewer component.

click to expand
Figure 2-13: An XMLViewer component

Note that a different image can represent each node type. For example, the image representing the root is different from the ones representing a branch and a leaf. Also, you can customize these images, meaning you choose your own images to pass to the XML viewer component. Also, a selected element has a yet different image.

The class that represents XML viewer components is XmlViewer. It inherits the System.Windows.Forms.TreeView class. As a result, XmlViewer automatically supports scrolling. You can find the code for the XmlViewer class in the XmlViewer.vb file.

The class has only two public methods: BuildTree and Clear. The BuildTree method builds a tree by passing a string representing the content of an XML document. The Clear method clears the component.

At the declarations part of the class, there are six integers representing the image indexes:

 Public RootImageIndex As Integer Public RootSelectedImageIndex As Integer Public LeafImageIndex As Integer Public LeafSelectedImageIndex As Integer Public BranchImageIndex As Integer Public BranchSelectedImageIndex As Integer 

The client must pass an ImageList object to the XmlViewer and then set the indexes for each of the previous six fields, such as in the following code:

 xmlViewer.ImageList = imageList xmlViewer.RootImageIndex = 8 xmlViewer.RootSelectedImageIndex = 9 xmlViewer.BranchImageIndex = 10 xmlViewer.BranchSelectedImageIndex = 11 xmlViewer.LeafImageIndex = 12 xmlViewer.LeafSelectedImageIndex = 13 

After you construct an instance of the XmlViewer class, the client calls the BuildTree method by passing the content of an XML document. This method will then build the tree. Nothing will be displayed if the XML document is not well-formed.

The BuildTree method will first clear the component by calling the Clear method:

 Clear() 

It then creates a System.Xml.XmlDocument:

 Dim xmlDocument As New XmlDocument() 

Next, the BuildTree method needs to call the Load method of the XmlDocument class to load the document. The Load method has four overloads to allow you to pass one of the following: a System.IO.Stream object, a String object, a System.IO.TextReader object, or a System.Xml.XmlReader object. You have a string that is passed to the BuildTree method, so naturally your choice to load an XML document will be to use the second overload that accepts a String object.

However, looking up the documentation, you will see the following signature of the second overload:

 Overridable Overloads Public Sub Load (ByVal filename As String) 

The method overload expects a filename to the XML document, not the content of the document. Therefore, you cannot use this one.

Our second choice is to use the first overload of the Load method that accepts a System.IO.TextReader. You can obtain a TextReader object from a String by constructing a System.IO.StringReader object because the System.IO.StringReader class is a child class of the System.IO.TextReader class. Therefore, you have the following:

 xmlDocument.Load(New StringReader(xmlText)) 

The BuildTree method draws the tree by calling the private DrawNode method. However, the DrawNode method needs the root element. Therefore, you create a root that is of type System.Windows.Forms.TreeNode. The TreeNode class has several constructors. Which constructor you choose largely depends on whether you want the element to display text and an image or text only. If you want the root to display both text and an image, the constructor that allows you to do so has the following signature:

 Public Sub New( _     ByVal text As String, _     ByVal imageIndex As Integer, _     ByVal selectedImageIndex As Integer _ ) 

You obtain the text for the root from the name of the XML document's root. In a System.Xml.XmlDocument object, the root element is represented by the DocumentElement property, which is of type System.Xml.XmlElement. The XmlElement class has the Name property representing the name of the element. Therefore, the following code creates a TreeNode object with some text and the appropriate indexes of the images to be displayed when the element is selected and when it is not selected:

 Dim root As New TreeNode(xmlDocument.DocumentElement.Name, _   RootImageIndex, rootselectedimageindex) 

Having a root, you can then call the DrawNode method, passing to the method the root of the XML document (xmlDocument.DocumentElement) the TreeNode object that represents the root and 0. The latter represents the level of depth of the XML element. The root has the level of 0.

What the DrawNode method does is to create a visual representation of the XML document. It will traverse the nodes in the XmlDocument object passed as the first argument and create a System.Windows.Forms.TreeNode object for each node in the document:

 DrawNode(xmlDocument.DocumentElement, root, 0) 

The call to the DrawNode method will create a hierarchical structure for the TreeNode root. Next, you need to add the root to the Nodes property of the current instance of the XmlViewer class. The Nodes property (which represents the collection of nodes) inherits from the TreeView class:

 Me.Nodes.Add(root) 

Finally, you call the ExpandAll method to expand all the nodes:

 Me.ExpandAll() 

Using the Find Dialog Box

The XML Editor application supports the searching and replacing of words, just like Microsoft Word lets you find and replace a word or a phrase. For this purpose, you will create a Find dialog box that will be called from a child document.

The application, which acts as an MDI container, can have multiple child documents, but all the child documents share the same Find dialog box because there needs only to be one anyway, just like all documents in Microsoft Word share the same Find and Replace dialog boxes.

To build a Find dialog box that can be shared, you use the Singleton pattern as explained previously. Figure 2-14 shows the Find dialog box. Note that for this application, the Find and Replace functions use the same dialog box.

click to expand
Figure 2-14: The Find dialog box

Note also that the Find dialog box does not have Minimize and Restore buttons.

You can find the FindDialog class, which represents the Find dialog box, in the FindDialog.vb file in the chapter's project directory.

First and foremost, as a Singleton the FindDialog class only has a private constructor:

 Private Sub New()   ... End Sub 

To obtain an instance of this class, you use an object factory such as the following:

 Public Shared Function GetInstance()   If findDialog Is Nothing Then     findDialog = New FindDialog()   End If   Return findDialog End Function 

The GetInstance method returns an instance of the FindDialog class. Because this method is static (shared), you can call it without having to have an instance of its class. In fact, this method must be static or no instance can be created otherwise. The object reference returned by this method (findDialog) is also declared shared:

 Private Shared findDialog As findDialog 

Another important thing to note is that the FindDialog object will remain in existence even when it is closed. Therefore, you override the OnClosing method as follows:

 Protected Overrides Sub OnClosing(ByVal e As CancelEventArgs)   e.Cancel = True 'doesn't allow the user to close   Me.Hide() End Sub 

Note that the Cancel property of the CancelEventArgs is set to True to cancel the closing. Instead of closing, you simply hide the form. When the Find dialog box is called again, the hidden instance reappears. As a result, it retains the previous states set by the user. For instance, if the Case Sensitive box was checked, that box will remain checked. If there is a value in the Find box, the value will also be displayed again.

You will also notice that you override the Dispose method in the FindDialog class, and you set the static object reference findDialog to Nothing:

 Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)   findDialog = Nothing   MyBase.Dispose(disposing) End Sub 

The reason why you need to set the findDialog to Nothing when the findDialog object is disposed is this: The GetInstance method is called from inside the child document represented by the Document class. To be exact, the code that calls it resides inside the overridden OnGotFocus method. This means a FindDialog instance is obtained every time that child document gets a focus, including when the child document is first created:

 Protected Overrides Sub OnGotFocus(ByVal e As EventArgs)   findDialog = FindDialog.GetInstance()   findDialog.Owner = Me   findDialog.SetTextArea(textArea) End Sub 

See that the child document that received focus always gets hold of the instance of the FindDialog class. You then set its Owner property to the child document itself, making the child document owner of the findDialog object. In addition, you pass the current child document's TextArea to the FindDialog instance for the FindDialog instance to work on. When the current child document with focus closes, the focus moves to another child document, and this next child document will be the owner of the FindDialog instance. Now, what if the last child document is closed? The FindDialog instance will belong to no child document, and because its owner is disposed, the FindDialog instance will also be disposed.

If you do not set the findDialog object reference to Nothing, it will still reference the disposed FindDialog object. As a result, when a new child document is created, it will call the following code in its OnGotFocus method:

 findDialog = FindDialog.GetInstance() 

This will call the GetInstance method of the FindDialog class:

 Public Shared Function GetInstance()   If findDialog Is Nothing Then     findDialog = New FindDialog()   End If   Return findDialog End Function 

Remember that the findDialog object reference is shared, so it is still in memory. If you do not set the findDialog object reference to Nothing when the instance of the FindDialog class is disposed, the findDialog in the GetInstance method will not evaluate to True in the If statement and the GetInstance method will simply return the findDialog object reference, which still references a disposed object.

Now, look at the second line of the OnGotFocus method in the Document class:

 findDialog.Owner = Me 

Trying to access the Owner property of a disposed object will throw an exception.

On the other hand, if the findDialog object reference is set to Nothing when the instance of the last FindDialog class is disposed, the GetInstance method will create a new instance and return this instance.

Now that you know the lifecycle of the class, shift your attention to the three important methods of the FindDialog class: Find, Replace, and ReplaceAll.

Using the Find Method

The Find method collects search options and calls the Find method of the StyledTextArea class. The FindDialog class's Find method collects search options from the following controls:

  • findTextBox: The search pattern

  • caseCheckBox: Whether case sensitivity matters

  • wholeWordCheckBox: Whether the search is to find the whole word only

  • upRadioButton: The direction of the search

In addition, the method also uses the x1 and y1 class variables to determine the start column and start line of the searching.

If the method finds a match, it highlights the matching pattern in the StyledTextArea control and the Find method returns True. If no match is found, the method returns False. Listing 2-24 shows the Find method.

Listing 2-24: The Find Method

start example
 Private Function Find() As Boolean   If Not textArea Is Nothing Then     Dim pattern As String = findTextBox.Text     If pattern.Length > 0 Then       Dim p As ColumnLine = textArea.Find(pattern, x1, y1, _         caseCheckBox.Checked, wholeWordCheckBox.Checked, upRadioButton.Checked)       If p.Equals(New ColumnLine(0, 0)) Then         'search not found         If upRadioButton.Checked Then           y1 = textArea.LineCount           x1 = textArea.GetLine(y1).Length + 1         Else           y1 = 1           x1 = 1         End If         MessageBox.Show("Search Pattern Not Found")         Return False       Else         If Not upRadioButton.Checked Then           x1 = p.Column + 1           y1 = p.Line         Else           x1 = p.Column - pattern.Length - 1           If x1 <= 1 Then             x1 = 1             y1 = p.Line - 1           Else             y1 = p.Line           End If         End If         Return True       End If     End If   End If   Return False End Function 
end example

The Find method starts by examining the length of the search pattern. It only does the search if the pattern is not blank. Searching is straightforward by calling the Find method of the StyledTextArea class, passing the search options you have collected:

 If pattern.Length > 0 Then   Dim p As ColumnLine = textArea.Find(pattern, x1, y1, _     caseCheckBox.Checked, wholeWordCheckBox.Checked, upRadioButton.Checked)     .     .     . 

The Find method of the StyledTextArea class returns a ColumnLine object indicating the start column and line of the matching pattern, if it finds one. Otherwise, the ColumnLine object is equal to ColumnLine(0, 0).

If no matching pattern is found, the FindDialog class's Find method will reposition x1 and y1 to the beginning or the end of the text in the StyledTextArea control, depending on whether the upRadioButton is checked:

 If upRadioButton.Checked Then   y1 = textArea.LineCount   x1 = textArea.GetLine(y1).Length + 1 Else   y1 = 1   x1 = 1 End If 

Then, it will display a message box saying that the search pattern was not found and return False:

 MessageBox.Show("Search Pattern Not Found") Return False 

If a matching pattern is found, the Find method will reposition the x1 and y1 variables for the next invocation of the Find method:

 If Not upRadioButton.Checked Then   x1 = p.Column + 1   y1 = p.Line Else   x1 = p.Column - pattern.Length - 1   If x1 <= 1 Then     x1 = 1     y1 = p.Line - 1   Else     y1 = p.Line   End If End If 

It will then return True:

 Return True 

Using the Replace Method

The Replace method replaces the highlighted text in the StyledTextArea control with the string in the replaceTextBox. Normally, the Replace method is called after the Find method is called and finds a match. Listing 2-25 shows the Replace method.

Listing 2-25: The Replace Method

start example
 Private Function Replace() As Boolean   If Not textArea Is Nothing Then     If textArea.SelectedText.Equals("") Then       Return Find()     Else       Dim replacePattern As String = replaceTextBox.Text       Dim buffer As IDataObject = Clipboard.GetDataObject()       textArea.Cut()       Clipboard.SetDataObject(replacePattern)       textArea.Paste()       Clipboard.SetDataObject(buffer)       Return Find()     End If   End If End Function 
end example

The Replace method starts by checking whether there is some highlighted text in the StyledTextArea control. If there is not, it calls the Find method. The user will then have to invoke the Replace method again once the Find method finds a matching pattern. This time it will do the code in the Else block. What it does is the following:

  1. Saves the current content of the Clipboard to a System.Windows.Forms.IDataObject object

  2. Calls the Cut method of the StyledTextArea class to remove the selected text from the StyledTextArea control

  3. Calls the SetDataObject method of the System.Windows.Forms.Clipboard class to copy replacePattern to the Clipboard

  4. Calls the Paste method of the StyledTextArea class to paste the data in the Clipboard at the position of the caret

  5. Calls the SetDataObject method again to copy the content of buffer (the original content of the Clipboard) back to the Clipboard

  6. Calls the Find method so that the next matching pattern is highlighted and ready to be replaced

Like the Find method, the Replace method returns True when a replace was successfully executed, or False otherwise.

Using the ReplaceAll Method

The ReplaceAll method repeatedly calls the Replace method to replace all matching patterns in the StyledTextArea control:

 Private Sub ReplaceAll()   While Replace()   End While End Sub 

Using the TextPrinter Class

The TextPrinter class offers text printing functionality that can be reused in various applications. The main advantage of using the TextPrinter class is that the code for printing will not scatter the main application classes. You will find the TextPrinter class in the TextPrinter.vb file. There are three public methods you can use: PrintPreview, SetupPage, and Print.

The PrintPreview and SetupPage methods are straightforward. If you have read the section "Printing in the .NET Framework Windows", you surely will understand it. The PrintPreview method declares and creates a System.Windows.Forms.PrintPreviewDialog object, sets its Document property with a PrintDocument object (printDoc), and calls its ShowDialog method:

 Dim dlg As New PrintPreviewDialog() dlg.Document = printDoc dlg.ShowDialog() 

Calling the ShowDialog method triggers the PrintPage event of the System.Drawing.Printing.PrintDocument object, which then executes the code in the PrintPage event handler (printDoc_PrintPage). This event handler will be explained later when discussing the Print method.

The SetupPage method declares and creates a System.Windows.Forms.PageSetupDialog object, sets some of its properties, and calls its ShowDialog method.

The Print method uses a System.Windows.Forms.PrintDialog class to let the user choose some options and then calls the Print method of the PrintDocument object. This captures two of the events that are raised when the Print method is called: BeginPrint and PrintPage. The BeginPrint event raises once at the beginning of the printing process. Its event handler (printDoc_BeginPrint) sets the startPrinting Boolean to True and prepares a StringReader textToPrint. The printDoc_PrintPage event handler(explained next) uses the startPrinting Boolean.

The PrintPage event triggers for each page printed. The printDoc_PrintPage event handler first checks the value of startPrinting. If startPrinting is True, the event handler calculates certain values that will be used throughout the printing of pages. It then iterates the StringReader textToPrint to print each line of the string.

Using the Document Class

The Document class represents a child document in the MDI container. Each instance of this class is assigned a unique number that is stored in its public field ChildIndex. The MDI container users this index mainly to synchronize the child document with its associated tab page.

You can use an instance of the Document class to edit and validate an XML document as well as to build the tree of the elements in the XML document. Three shortcuts are provided: F5 and F6 for validating the XML document and F9 for building the tree. F5 validates XML documents that do not contain a schema. F6 validates XML documents having an inline schema. The main method of this class is the ValidateDocument function.

Before delving into the class, however, you should look at the delegate TitleEventHandler and the event argument class TitleEventArgs used by the Document class. You can find both in the Document.vb file.

You use TitleEventHandler and TitleEventArgs when you trigger the custom event TitleChanged. Why you need these two is explained shortly.

The MDI container creates a tab page for each child document opened or each child document created. Each tab page is associated with one child document. The text in a tab page is the same as the text shown on the title bar of the child document. The child document title is the name of the XML document file. The child document title can change in two cases:

  • A new document is given a temporary name starting with "Document"; for example, the first new document is called Document 1. When the user saves a document, the user can give it a filename that is different from its temporary name, and this will be reflected in the title bar.

  • The user can save a document as another name. This too will change the name.

The tab page associated with a child document resides in the MDI container, and the child document knows nothing about it. When the child document's title changes, the text in the associated tab page must change too. The child document notifies the MDI container by raising the TitleChanged event. This event must then be captured in the MDI form.

The Document class extends the System.Windows.Forms.Form class and uses the StyledTextArea control discussed in Chapter 1, "Creating a Custom Control: StyledTextArea". You can find the Document class in the Document.vb file.

The Document class also uses the FindDialog class for searching and the TextPrinter class for printing the text.

Additionally, the Document class has an object reference to an XmlViewer and an object reference to a StatusBar control. When a Document object is constructed, the MDI container passes the references of its XmlViewer and StatusBar objects to the child document. Having an XmlViewer, the child document can build a tree of the XML elements. With a status bar, it can also write to the MDI container's status bar panels. The child document needs to know nothing about the MDI container, which promotes its code reuse.

The object reference to an XmlViewer is passed to the constructor. You pass the status bar by setting the StatusBar property. Because there is no guarantee that an MDI container will set the StatusBar property, access to the status bar object is always first, making sure that the object is not null.

The Document class has two constructors. Their signatures are as follows:

 Public Sub New(ByVal childIndex As Integer, ByVal filePath As String, _   ByRef xmlViewer As xmlViewer) Public Sub New(ByVal childIndex As Integer, ByRef xmlViewer As xmlViewer) 

The two constructors enforce that a Document object always has a child index and a reference to the XmlViewer control.

The following sections describe the Document class's members.

Understanding the Document Class's Properties

The Document class has three properties: StatusBar, Text, and Title.

StatusBar

The MDI container uses the write-only StatusBar property to pass an object reference to a StatusBar object so that the child document can write to the panels of the MDI container's status bar.

Text

The Text property represents the text in the StyledTextArea control. This property overloads the Text property inherited from the System.Windows.Forms.Control class.

Title

The Title property represents the text displayed on the document title bar. In the System.Windows.Forms.Control class this is represented by the Text property; therefore this Title property reads from and writes to the base class's Text property.

Understanding the Document Class's Methods

The following are the Document class's methods, not including the event handlers, which are self-explanatory.

Find

You invoke the Find public method when the user needs to use the Find and Replace facility. What this method does is display the FindDialog object, which was obtained or created in the OnGotFocus method.

GetTextPrinter

The private GetTextPrinter method returns a TextPrinter object that can be used to print and print preview the document's text or set the page. You call this method from the Print, PrintPreview, and SetupPage methods.

InitializeComponent

The private InitializeComponent method initializes components that are used from the Document object. The constructors call this method.

IsFilenameUsed

The private IsFilenameUsed method checks if a filename has been used by other child documents. To do this, the method needs to access the parent form through the MdiParent property. The MdiParent property returns a System.Windows.Forms.Form object. Once obtained, all the child documents can be obtained using the MdiChildren property. A call to the CType function converts a Form object (representing a child document) into a Document object. Because the Document class has the public FilePath field that holds the value of its file path (or null if the child document has not been saved), the IsFilenameUsed method can check the FilePath value of all child documents of the MDI container.

OnClosing

The protected OnClosing method overrides the same method in the base class. The reason why you override this method is to avoid an unsaved document closed without first warning the user that the document has not been saved. If a document has been changed but not saved, the Edited property of the StyledTextArea control will be True. If this is the case, a message box is displayed, prompting the user to either save it or confirm that they want to close the document without saving.

OnGotFocus

You call the OnGotFocus method when the document gets the focus. When it does, it needs to grab the singleton FindDialog method from another child document.

OnTitleChanged

The OnTitleChanged method raises the TitledChanged event.

Print

The private Print method calls the Print method of the TextPrinter class.

PrintPreview

The private PrintPreview method calls the PrintPreview method of the TextPrinter class.

RebuildTree

The private RebuildTree method calls the BuildTree method of the XmlViewer object and passes to it the Text property.

Save

The private Save method saves a document into a file. First it checks if the document has been saved before. A document that has been saved will have a file path; therefore a document whose file path is null has not been saved before. In this case, it calls the SaveAs method:

 If FilePath Is Nothing Then   Return SaveAs() Else   .   .   . End If 

If the document has been saved, you create a System.IO.StreamWriter object, passing the file path:

 Dim sw As New StreamWriter(FilePath) 

You then use its Write method to write it to the file, passing the string to be written:

 sw.write(Text) 

Afterward, you close the StreamWriter object, you set the Edited property of the StyledTextArea control to False, and you call the WriteToLeftPanel method:

 sw.Close() textArea.Edited = False WriteToLeftPanel("Document saved") 

On a successful save, the method returns True. Otherwise, it returns False.

SaveAs

The private SaveAs method saves a document that has not been saved before. It returns True if the document is saved successfully and False if the user cancels the saving.

To save a document, the user will be prompted to enter a filename using a System.Windows.Forms.SaveFileDialog:

 Dim saveFileDialog As New SaveFileDialog() saveFileDialog.Filter = "Xml Documents (*.xml)|*.xml|All files (*.*)|*.*" saveFileDialog.FilterIndex = 1 

Then, if the user clicks OK after entering a filename, it will check if the filename has been used in another child document by calling the IsFilenameUsed method:

 If saveFileDialog.ShowDialog = DialogResult.OK Then   'does not allow the doc to be saved as a name   'of a file that is opened as another child document   If IsFilenameUsed(saveFileDialog.FileName) Then     MessageBox.Show ( _       "The name is identical with one of the open documents. " & _       "Please use another name")     Return False     .     .     . 

If the filename is acceptable, the entered FileName will be set to the FilePath variable and the document title changed, and it invokes the OnTitleChanged to raise the TitleChanged event:

 Else   FilePath = saveFileDialog.FileName   Me.Title = Path.GetFileName(FilePath)   OnTitleChanged(New TitleEventArgs(Me.Title)) 

Lastly, it calls the Save method to do the actual saving of the document:

 Return Save() 

SelectLine

The public SelectLine method returns a string in the specified line number.

UpdateLineAndColumnNumbers

The private UpdateLineAndColumnNumbers method writes the line and column position of the caret in the StyledTextArea control to the right panel of the status bar.

SetupPage

The private SetupPage method calls the SetupPage method of the TextPrinter object.

ValidateDocument

The ValidateDocument method validates the current document. You can validate two types of XML documents: documents with an internal or external DTD and documents with an inline schema. This method accepts a Boolean to switch between the two validation types. A True value means that the document to be validated contains an inline schema; a False value means that the document to be validated uses a DTD.

This method first checks that the document has been saved. Otherwise, the SaveAs method is called and, upon a successful save, calls itself:

 If FilePath Is Nothing Then   If SaveAs() Then     Return ValidateDocument(withInlineSchema)   Else     Return False   End If End If 

An XML document often references an external DTD document. It is always assumed that this external document resides in the same directory as the XML document. Therefore, the first thing to do is to set the CurrentDirectory property of the System.Environment object to the XML document file path:

 Environment.CurrentDirectory = Path.GetDirectoryName(FilePath) 

It then makes sure that the document is not empty:

 Dim xmlText As String = Me.Text If xmlText.Trim().Equals("") Then   MessageBox.Show("Document is empty.", "Error")   Return False End If 

Next, it does the validating using the System.Xml.XmlValidatingReader class's Read method. Listing 2-26 shows the code that does the validation.

Listing 2-26: Validating Using Read

start example
 Dim xmlReader As XmlTextReader Dim xmlValReader As XmlValidatingReader Try   xmlReader = New XmlTextReader(New StringReader(xmlText))   If withInlineSchema Then     xmlValReader = New _       XmlValidatingReader(xmlText, XmlNodeType.Element, Nothing)     Else       xmlValReader = New XmlValidatingReader(xmlReader)     End If     While xmlValReader.Read()     End While     MessageBox.Show("Document validated.", "Validating Document")     Return True 
end example

This catches any exception during the validation process so that the user can find where the error is. An error message can include the line and column position of where the error occurs. In this case, you can notify the user by parsing the error message and highlighting the line (see Listing 2-27).

Listing 2-27: Parsing the Error

start example
 Catch ex As Exception   Dim errorMessage As String = ex.Message   ' it ends with "Line x, position y" or "(x, y)"   MessageBox.Show(errorMessage, "Error validating document")   Dim index1, index2 As Integer   Dim line As String   index1 = errorMessage.LastIndexOf("Line")   Try     If index1 = -1 Then       index1 = errorMessage.LastIndexOf("(")       index2 = errorMessage.LastIndexOf(",")       line = errorMessage.Substring(index1 + 1, index2 - index1 - 1)     Else       index2 = errorMessage.LastIndexOf(", position")       line = errorMessage.Substring(index1 + 5, index2 - index1 - 5)     End If     SelectLine(CInt(line))   Catch 
end example

WriteToLeftPanel

The private WriteToLeftPanel method writes to the first panel in the status bar, should a status bar be passed to the Document object. Because there is no guarantee an MDI container will pass a status bar, the WriteToLeftPanel method checks the statusBarField before it writes to its first panel:

 If Not statusBarField Is Nothing Then   If statusBarField.Panels.Count > 0 Then     statusBarField.Panels(0).Text = s   End If End If 

WriteToRightPanel

The private WriteToRightPanel method writes to the second panel in the status bar, should a status bar be passed to the Document object. Because there is no guarantee an MDI container will pass a status bar, the WriteToRightPanel method checks the statusBarField before it writes to its second panel:

 If Not statusBarField Is Nothing Then   If statusBarField.Panels.Count > 1 Then     statusBarField.Panels(1).Text = s   End If End If 

Understanding the Document Class's Event

The Document class has one event: TitleChanged. This event raises when the text in the title bar of the document changes.

Creating the MDI Container

The last part of the project is the XMLEditor class, which represents the MDI container. This class integrates all other parts. Figure 2-15 shows the XML editor application.

click to expand
Figure 2-15: An XML editor application

You can find the code for the XMLEditor class in the form1.vb file.

The XMLEditor class uses several images that are stored in the images subdirectory of the application directory. These images are for the parent form icon, the child document icon, the toolbar buttons, and the elements in the XmlViewer control.

The XMLEditor class manages its child documents with the help of the private childIndex field that is initialized as 1. Each child document is assigned a unique index number and is linked with a tab page having the same index number.

Because a TabPage does not have a property to hold an index number, you define a class called IndexedTabPage as an inner class of the XMLEditor class. You make IndexedTabPage an inner class because only the XMLEditor class uses IndexedTabPage.

Listing 2-28 shows the IndexTabPage class.

Listing 2-28: IndexTabPage

start example
 Class IndexedTabPage : Inherits TabPage   Private childIndexField As Integer   Public Property ChildIndex() As Integer     Get       Return childIndexField     End Get     Set(ByVal index As Integer)       childIndexField = index     End Set   End Property End Class 
end example

The IndexedTabPage class does not do much except add a property called ChildIndex to the TabPage class.

The declaration part of the XMLEditor class contains variables for the menu, menu items, toolbar, toolbar buttons, status bar, status bar panels, bitmaps, an XmlViewer, a Splitter, and an ImageList. (The use of these controls will not be explained again here.)

The XMLEditor class inherits the System.Windows.Forms.Form class; therefore, XMLEditor is a form. In order for it to be an MDI container, its IsMdiContainer is set to True, as in the first line of the InitializeComponent method:

 Me.IsMdiContainer = True 

The following sections mention some important methods of the XMLEditor class. Some of the methods, including most of the event handlers, are self-explanatory and will not be repeated here.

Understanding the XMLEditor Class's Methods

The following are the methods in the XMLEditor class.

AddTabPage

The private AddTabPage method adds an IndexedTabPage to the tabControl control. It accepts a String containing the filename of the child document associated with this tab page. Every tab page has an icon that is the eighth icon in the TabControl's ImageList.

The AddTabPage method starts by instantiating an IndexedTabPage object:

 Dim tabPage As New IndexedTabPage() 

It then sets its Text property of the TabPage with the filename of the path passed to this method, its ImageIndex to 7, and its ChildIndex with the current sequential index number:

 tabPage.Text = Path.GetFileName(text) tabPage.ImageIndex = 7 tabPage.ChildIndex = childIndex 

Next, the method increments the child index so that the next tab page will have a different index number:

 childIndex += 1 

The tab page is ready to be added to the tab control. You add a tab page to a tab control by calling the Add method of the tab control's Controls collection:

 tabControl.Controls.Add(tabPage) 

The new tab page will be the one selected. So, you set the tab control's SelectedIndex property with the tab page position in the Controls collection. Because the new tab page is the most recently added tab page, it will be last in the position—or the same as the number of tab pages in the tab control's Controls collection, which is represented by the TabCount property. However, the SelectedIndex property is zero-based, so the last position will be the number of tab pages minus one:

 tabControl.SelectedIndex = tabControl.TabCount - 1 

child_TitleChanged

The child_TitleChanged event handler invokes every time a child document's title changes. This event handler handles the Document class's TitleChanged event. The reason why you need to capture this event is to update the tab page's Text when its associated child document has a new title.

The first thing to do, of course, is to obtain the Document object that sends the event message. This is readily available from the first argument to the child_TitleChanged event handler. Using CType, you can convert the Object object to a Document object:

 Dim doc As Document = CType(sender, Document) 

Then, you can get the child index of the child document:

 Dim childIndex As Integer = doc.ChildIndex 

Now you need to find the tab page that corresponds to the child document that sends the event message. You do this by comparing the child index of the document with that of each of the tab pages. When a match is found, you know the corresponding tab page has been found, so you can change its Text property with the new title of the child document:

   Dim tabPage As IndexedTabPage   For Each tabPage In tabControl.TabPages     If tabPage.ChildIndex = childIndex Then       tabPage.Text = e.Title       Exit For     End If   Next End Sub 

child_Closed

The child_Closed event handler handles the Close event of every child document. child_Closed therefore is called every time a child document is closed. The purpose of capturing this event is to remove the tab page associated with the closed child document.

It starts by converting the sender (the closed document) to an object of type Document using the CType function and obtains the ChildIndex property of the closed document:

 Dim doc As Document = CType(sender, Document) Dim childIndex As Integer = doc.ChildIndex 

Once you get the child index, you have to find the corresponding tab page by iterating the TabPages collection of the tab control and matching its child index with the closed document's child index. Once a match is found, the tab page is removed from the TabPages collection using the Remove method:

 'now remove tabpage with the same childIndex Dim tabPage As IndexedTabPage For Each tabPage In tabControl.TabPages   If tabPage.ChildIndex = childIndex Then     tabControl.TabPages.Remove(tabPage)     Exit For   End If Next 

GetDocumentByFilepath

The GetDocumentByFilepath method returns a Document object having the specified path. It iterates the MdiChildren property and compares its FilePath field with the specified path:

 Dim childCount As Integer = Me.MdiChildren.Length Dim i As Integer For i = 0 To childCount - 1   Dim doc As Document = CType(Me.MdiChildren(i), Document)   Dim docPath As String = doc.FilePath   If Not docPath Is Nothing Then     'docPath could be null in the case of a new document     If docPath.Equals(path) Then       Return doc     End If   End If Next Return Nothing 

NewDocument

The NewDocument method creates a new child document and adds a corresponding tab page for the added child document.

The first line of the NewDocument method uses the Document constructor, passing the current child index and the object reference to the XmlViewer object:

 Dim doc As Document = New Document(childIndex, xmlViewer) 

For each child document, you wire two events: Closed and TitledChanged:

 AddHandler doc.Closed, AddressOf child_Closed AddHandler doc.TitleChanged, AddressOf child_TitleChanged 

Then, you must set the child document's MdiParent to the MDI container. You also pass the reference of the parent form's status bar to the child document so that the child document can write to the status bar:

 doc.MdiParent = Me doc.StatusBar = Me.statusBar 

Next, you show the child document using the Show method:

 doc.Show() 

Last, you add a tab page by calling the AddTabPage method:

 AddTabPage("Document " & childIndex.ToString()) 

OnMdiChildActivate

The OnMdiChildActivate method overrides the OnMdiChildActivate method in the base class. The reason you need to override this method is so you can select the tab page associated with the active child document.

One thing to be cautious about is to avoid the infinite loop caused by the tabControl_SelectedIndexChanged that triggers when the selected tab page changes. The tabControl_SelectedIndexChanged activates the child document associated with the tab control with the selection. A flag (childFormActivated) is used and its value is set to True in this method. The tabControl_SelectedIndexChanged event handler will not activate a child document if this flag is True.

The OnMdiChildActivea method starts by calling the overridden method in the base class:

 MyBase.OnMdiChildActivate(e) 

It then sets the childFormActivated Boolean to True so that the tabControl_SelectedIndexChanged event handler will not try to activate a child document, which in turn will cause the OnMdiChildActivate to be invoked, causing an infinite loop:

 childFormActivated = True 

Then it obtains the active child document, gets the child index of the active child document, and sets the focus to the active child document:

 Dim activeChildDocument As Document = CType(Me.ActiveMdiChild, Document) 'Get the ChildIndex of the active MdiChild Dim activeChildIndex As Integer = activeChildDocument.ChildIndex activeChildDocument.Focus() 

Next, it goes through the tab pages collection to find the tab page with the same child index and selects the tab page:

 Dim tabPage As IndexedTabPage Dim i As Integer Dim tabPageCount As Integer = tabControl.TabCount For i = 0 To tabPageCount - 1   tabPage = tabControl.TabPages(i)   If tabPage.ChildIndex = activeChildIndex Then     tabControl.SelectedIndex = i     Exit For   End If Next i 

Lastly, the OnMdiChildActivate rebuilds the tree in the XmlViewer object so it draws the hierarchical data of the active child document:

 xmlViewer.BuildTree(CType(Me.ActiveMdiChild, Document).Text) 

OpenDocument

You use the private OpenDocument method to open an XML document. The method enables the user to navigate the file system using System.Windows.Forms.OpenFileDialog. If the user tries to open a document that is already open, the method will activate the existing document instead.

The method starts by constructing an OpenFileDialog object and setting some of its properties:

 Dim openFileDialog As New OpenFileDialog() 'you can set an initial directory if you want, such as this 'openFileDialog.InitialDirectory = "c:\" openFileDialog.Filter = "Xml Documents (*.xml)|*.xml|All files (*.*)|*.*" openFileDialog.FilterIndex = 1 

After the user clicks the OK button, the method first tries to retrieve a document having the same file path from the collection of all child documents using the GetDocumentByFilePath method. If there is already an open document by that name, the document is activated instead:

 If openFileDialog.ShowDialog() = DialogResult.OK Then   'check if the document already opened by this app.   Dim doc As Document = GetDocumentByFilepath(openFileDialog.FileName)   If Not doc Is Nothing Then     doc.Activate()   Else 

Otherwise, the OpenDocument method tries to open that document into a System.IO.Stream object:

 Dim stream As Stream ' the current XML document xmlDocumentFilePath = openFileDialog.FileName stream = openFileDialog.OpenFile() 

To read the document, you create a System.IO.StreamReader object, passing the stream resulting from the OpenFile method of the OpenFileDialog object:

 Dim sr As New StreamReader(stream) 

Then, you create a Document object and write the Closed and TitleChanged events to the child_Closed and child_TitleChanged event handlers, respectively:

 doc = New Document(childIndex, xmlDocumentFilePath, xmlViewer) AddHandler doc.Closed, AddressOf child_Closed AddHandler doc.TitleChanged, AddressOf child_TitleChanged 

Next, you populate the child document's StyledTextArea control by reading the StreamReader object:

 doc.Text = sr.ReadToEnd() sr.Close() 

Afterward, you set the MdiParent property of the child document, pass the status bar to the StatusBar property, and call the Show method to display the child document:

 doc.MdiParent = Me doc.StatusBar = Me.statusBar doc.Show() 

Lastly, you add a tab page that is associated with the child document using the AddTabPage method:

 AddTabPage(xmlDocumentFilePath) 

tabControl_SelectedIndexChanged

The tabControl_SelectedIndexChanged event handler invokes every time a different tab page is selected in the tab control—for example, when the user clicks one of the tab pages. When a tab page is selected, the child document associated with it must also been activated. Therefore, you need to first obtain the child index of the selected tab page and then find the child document having the same child index and activate it.

Note that the SelectedIndexChanged can also raise when the user selects a child document. Selecting a child document will cause the OnMdiChildActivate method to be executed, and the overridden OnMdiChildActivate method in the XMLEditor class sets the SelectedIndex property of the Tab control, causing the SelectedIndexChange event to be raised. To prevent this infinite loop, the tabControl_SelectedIndexChanged changed event handler is only executed when the childFormActivated flag is False:

 If Not childFormActivated Then ' this is to avoid infinite loop 

The tabControl_SelectedIndexChanged event handler obtains the child index of the tab page selected, iterates through the collection of the child documents, and compares the child index with the child index of each child document:

 'get the ChildIndex of the active tab page If tabControl.SelectedIndex <> -1 Then '-1 when if is no tabpage   Dim tabPage As IndexedTabPage = _     CType(tabControl.TabPages(tabControl.SelectedIndex), IndexedTabPage)   Dim activeChildIndex As Integer = tabPage.ChildIndex '   ' Now activate the MdiChild with the same ChildIndex   Dim mdiChild As Document   For Each mdiChild In Me.MdiChildren     If mdiChild.ChildIndex = activeChildIndex Then 

Once a match is found, the child document activates:

 mdiChild.Activate() Exit For 

At the end of the event handler, you set the childFormActivated flag to False:

 childFormActivated = False 

toolBar_ButtonClick

The toolBar_ButtonClick event handler executes when a user clicks a toolbar button. It first finds out which button was clicked and then does a specific task depending on the button clicked:

 ' Evaluate the Button property to determine which button was clicked.     If e.Button.Equals(newToolBarButton) Then       fileNewMenuItem.PerformClick()     ElseIf e.Button.Equals(openToolBarButton) Then       fileOpenMenuItem.PerformClick()     ElseIf e.Button.Equals(saveToolBarButton) Then       If Me.MdiChildren.Length > 0 Then         SendKeys.Send("%FS")       End If     ElseIf e.Button.Equals(printToolBarButton) Then       If Me.MdiChildren.Length > 0 Then         SendKeys.Send("%FP")     End If     ElseIf e.Button.Equals(cutToolBarButton) Then       If Me.MdiChildren.Length > 0 Then         SendKeys.Send("%ET")       End If     ElseIf e.Button.Equals(copyToolBarButton) Then       If Me.MdiChildren.Length > 0 Then         SendKeys.Send("%EC")       End If     ElseIf e.Button.Equals(pasteToolBarButton) Then       If Me.MdiChildren.Length > 0 Then         SendKeys.Send("%EP")       End If     End If 

Compiling and Running the Application

To compile the project files, follow these steps:

  1. Create a working directory.

  2. Copy StyledTextArea.dll from Chapter 1 to the working directory.

  3. Copy the XmlViewer.vb, FindDialog.vb, Document.vb, TextPrinter.vb, and Form1.vb files into the working directory.

  4. Create the images directory in that directory and copy all the image files into the images directory.

  5. Run the build.bat file to build the project.

Listing 2-29 shows the content of the build.bat file.

Listing 2-29: build.bat

start example
 vbc /t:library /r:System.dll,System.Windows.Forms.dll, _ System.Drawing.dll,StyledTextArea.dll FindDialog.vb vbc /t:library /r:System.dll,System.Windows.Forms.dll, _ System.Drawing.dll,System.Xml.dll XmlViewer.vb vbc /t:library /r:System.dll,System.Windows.Forms.dll, _ System.Drawing.dll,System.Xml.dll TextPrinter.vb vbc /t:library /r:System.dll,System.Windows.Forms.dll, _ System.Drawing.dll,mscorlib.dll,System.Xml.dll, _ StyledTextArea.dll,XmlViewer.dll,TextPrinter.dll, _ FindDialog.dll Document.vb vbc /t:winexe/r:System.dll,System.Windows.Forms.dll, _ System.Drawing.dll,mscorlib.dll,System.Xml.dll,StyledTextArea.dll, _ XmlViewer.dll,TextPrinter.dll,FindDialog.dll,Document.dll form1.vb 
end example

To run the application, type form1.exe in the command prompt.




Real World. NET Applications
Real-World .NET Applications
ISBN: 1590590821
EAN: 2147483647
Year: 2005
Pages: 82

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net