Chapter One: A Sample Program and Inversion of Control

My name is Bill, and I am an integration newbie.

The purpose of my blog is to chronicle my experiences as I work to integrate Visual Studio Tools for Applications into my own program. Hopefully, this will help other programmers get started with VSTA integration.

The purpose of the sample in this particular blog post is to introduce the application I will be integrating with VSTA, and to introduce the concept of Inversion of Control and its benefits to software development. 

Here is a brief and very basic explanation of Inversion of Control:

 Imagine a class named ClassA. ClassA is the main class for an application and has dependencies on two other classes, Class 1 and Class 2:

Class ClassA

    Private x, y, a, b, c As Integer

    Private str As String

    Sub Main()

        MyClass1 As New Class1

  MyClass1.DoSomething(x, y)

  MyClass2 As New Class2

        MyClass2.DoSomethingElse(a, b, c)

        str = MyClass2.DoEvenMore()

        Console.WriteLine(str)

    End Sub

End Class

If the programmer wants to make any significant changes to either of these dependencies, they must also change the main class’s code. For example, if the programmer changed Class1, he would also have to change the code in ClassA for his program to compile, let alone run! The programmer wants to be able to change the code in Class1 or Class2 without altering ClassA. Right now, this is not possible because of ClassA’s dependencies on Class1 and Class2. The way to make this possible is to by use Inversion of Control.  Inversion of Control, at its most basic level, is, instead of having Class A run methods in Classes 1 and 2, Classes 1 and 2 would actually run (call into) methods in Class A, the main class.


Class A is the main application, so the programmer will not want to change that, just some of its dependencies (Classes 1 and 2).  All ClassA has to do is start both of them, and then allow them to control ClassA. Instead of ClassA controlling them, as is normal, Classes 1 and 2 are controlling Class A.

To start the process of implementing Inversion of Control with VSTA, I made a relatively simple application called “Contact Manager,” which does precisely what its title would imply. The user can control this application by either using the GUI or by letting the application run silently from a script. The script has the ability to add, delete and edit contacts based on user-generated data in text file in the program’s directory. It will also normalize all contacts in the database (capitalize the first and last names) and print out a sorted, formatted list of all contacts in the database.

The GUI has printing as well as adding, deleting and editing capabilities. When contacts are added or edited (in both script mode and interface mode), they are automatically normalized (the first and last names are capitalized).  Another feature of the interface is its searching ability. The user can search for all or part of the first or last name of a contact, and the app will display all the results to the user.

            Now that we’ve gone over the program’s functionality, let’s look deeper at the code itself.  I have attached my source code for this project and I would encourage you to look at that as well.

  

The MainForm.vb file has three main classes: ContactClass, ContactApp and ContactObj. Let’s walk through their important parts:

1.      ContactClass: the class that handles most of the behind the scenes work. It updates and alters the data in the database based on either script commands or interface commands. This class also contains a sub-class, ContactEventArgs, which defines the arguments for all the events in ContactClass.  Here are the methods for ContactClass:

Public Sub AppLoad()

Asks if the user wants to run the app in script mode, then takes appropriate action based on the user’s input.

Public Sub UpdateDataset()

Updates the dataset and the array.

Private Function GetContactID

(ByVal Contact As ContactObj)

As Integer

Gets the automatically assigned ID for the specified contact from the dataset

Public Sub AddContact(ByVal

 Contact As ContactObj)

Adds the contact to the database, or gives an error if the contact already exists

Public Sub DeleteContact(ByVal

 Contact As ContactObj)

Deletes the contact from the database, or gives an error if the contact does not exist (Script mode only).

Public Sub EditContact(ByVal

 OldContact As ContactObj,

 ByVal NewContact As

 ContactObj)

Edits the contact, or gives an error.

Private Sub NormalizeContact

(ByRef Contact As ContactObj)

Capitalizes the first letter of the first and last name values in the contact.

Public Sub PrintList()

Prints a sorted list of all the contacts in the database if the database is not empty.

                    

Public Sub

 PrintDocument1_PrintPage

(ByVal sender As

 System.Object, ByVal e As

 System.Drawing.Printing.

PrintPageEventArgs)

Creates the contact list page(s) of sorted contacts.

 

Let’s take a closer look at the AppLoad method:

 

    ContactApp class's ContactApp_Load method calls this method. It asks if the user wants to run the app in script mode if yes, the Connect method on ContactAutoClass is called and passed ContactClass.

    Public Sub AppLoad()

        UpdateDataset()

        Dim MBResult = MsgBox("Would you like to run this program from Script?" + Environment.NewLine + "(If Yes, the Interface will be hidden.)", MsgBoxStyle.YesNo)

        Select Case MBResult

            Case MsgBoxResult.Yes

                ScriptBool = True

                MyContactAuto.Connect(Me)

                ContactApp.Close()

            Case MsgBoxResult.No

                ScriptBool = False

        End Select

    End Sub

End Class

 

2.      ContactApp is the main class for the User Interface. Updates but does not edit content.  All other interface forms in the application behave in a way similar to this.

If the user runs the program in Script mode, the only methods in this class that are used are ContactApp_Load and PrintDocument1.Print.

If the user runs the program in interface mode, this class becomes the bridge between the actual interface and the behind-the-scenes work: ContactApp calls the right methods in ContactClass based on what the user selects in the interface.

Private Sub ContactApp_Load

(ByVal sender As System.Object,

ByVal e As System.EventArgs)

Handles MyBase.Load

When the app starts, this method centers the app to the screen, calls AppLoad in ContactClass, and populates the list box.

Private Sub

AddContactToolStripMenuItem_Click

(ByVal sender As System.Object,

ByVal e As System.EventArgs)

Handles

AddContactToolStripMenuItem.Click

When the Add Contact tool strip item is clicked, this method loads the AddContactForm.

Private Sub

EditContactToolStripMenuItem_Click

(ByVal sender As System.Object,

ByVal e As System.EventArgs)

Handles

EditContactToolStripMenuItem.Click

Brings  up the Edit Contact Form if the user has selected a contact in the list box.

Private Sub

DeleteContactToolStripMenuItem_Click

(ByVal sender As System.Object,

ByVal e As System.EventArgs)

Handles

DeleteContactToolStripMenuItem.Click

Deletes a selected contact if the user has selected a contact in the list box.

Private Sub

AboutToolStripMenuItem_Click

(ByVal sender As System.Object,

ByVal e As System.EventArgs)

Handles

AboutToolStripMenuItem.Click

Shows the About box.

Private Sub

ExitToolStripMenuItem_Click

(ByVal sender As System.Object,

ByVal e As System.EventArgs)

Handles

ExitToolStripMenuItem.Click

Closes the app.

Private Sub

FindToolStripMenuItem_Click

(ByVal sender As System.Object,

ByVal e As System.EventArgs)

Handles

FindToolStripMenuItem.Click

Shows the Find dialog.

Private Sub

ListBox1_SelectedIndexChanged

(ByVal sender As System.Object,

ByVal e As System.EventArgs)

Handles

ListBox1.SelectedIndexChanged

Shows the contact values in their respective fields when the user selects a contact in the list box.

Private Sub

PrintContactListToolStripMenuItem_Click

(ByVal sender As System.Object,

ByVal e As System.EventArgs)

Handles

PrintContactListToolStripMenuItem.Click

Shows a preview of the document to be printed.

Public Sub FillListBox()

Populates the list box on the main form.

Private Sub ListBox1_KeyDown

(ByVal sender As Object, ByVal

e As

System.Windows.Forms.KeyEventArgs)

Handles ListBox1.KeyDown

Allows the user to use the Delete, Insert, and F1 keys to Delete a selected contact, Add a new contact, and show the About Box, respectively.

 

 

3.      ContactObj is the class that defines what a Contact is and provides methods for accessing and altering Contact data. Other than methods for accessing and altering its properties and an overload of the CompareTo method, the ContactObj class has no methods:

Public Overloads Function CompareTo(ByVal obj As Object) As

Integer Implements IComparable.CompareTo

Public Property FirstName()

Public Property LastName()

Public Property HomePhone()

Public Property CellPhone()

Public Property WorkPhone()

Public Property Email()

 

 

The other important file is ContactAuto.vb, which contains ContactAuto. The methods in ContactAuto only run when the app is in Script mode. This file has only one class, ContactAuto. Let me direct your attention to the methods Connect and Run.  ContactApp calls both these methods. Look at ContactApp in MainForm.vb for a moment. Notice first how ContactApp_Load calls a method called AppLoad in ContactClass. AppLoad gets user input on whether or not to run the app in script mode. If the user decides to run the app in script mode, AppLoad calls Connect in ContactAuto and passes in ContactClass. Connect stores ContactClass, and gets the contact array from its instance of ContactClass and stores it separately. It then calls the Run method. The Run method controls all of the automation of ContactClass.  Other than connect and the various event handlers, Run calls all of the methods in ContactAuto. ContactAuto as a whole has four main automation abilities: It can add, delete and edit contacts based on text files, and will create those text files if they do not exist in the Contact Manager directory. AddFromFile, DeleteFromFile, and EditFromFile define the procedures for reading the text files, but methods in ContactClass carry out any changes to the actual database. The second automation ability is the normalization (capitalization of the contact’s first and last name) of all contacts in the ContactArray and the Database. Whenever ContactClass adds or edits a contact it automatically normalizes that contact, but if the user decides to alter the code to connect to a different database, whoever or whatever originally inputted that data might not have normalized the data. The NormalizeAllContacts method ensures that no matter what database is used, the app will have normalized all its contacts.

The third automation ability is printing a list of all contacts in the database. The actual page setup is defined in ContactClass, and this setup is used both by User Interface printing and script printing. The script differs from the interface by printing the list automatically instead of showing the user a preview. This, along with the other methods in Run, allows the user to start the program, and then have the program run without user intervention once the user selects yes in this message box:

The last automation ability is the logging of all actions taken by the script, as well as any errors encountered, and the date and time of these events. Each logging method is a handler for an event that an action raises when it either completes successfully or encounters a problem. The app saves all logging to Log.txt, which the app creates if it does not already exist. The app will not overwrite this log file in subsequent runs of the program. From the time that the log file is created, all data written to the log file stays in it unless cleared by the user.

 

Public Sub Connect(ByRef

TheContact As ContactClass)

Gets an instance of ContactClass and creates an instance of an array that is in that class. It also creates the log file.

Public Sub Run()

Creates handlers for the events in this class and ContactClass, and calls the methods for contacts to be added, deleted, edited, normalized, and printed.

Private Sub AddFromFile()

Imports contacts from a text file and adds them to the the database through ContactClass.AddContact.

Private Sub DeleteFromFile()

Imports contacts from a text file and deletes them from the the database through ContactClass.DeleteContact.

Private Sub EditFromFile()

Imports contacts to be edited from a text file and applies the changes to the database through ContactClass.EditContact.

Private Sub

ReplaceNullValues(ByRef

Contact As ContactObj)

Changes null values in each imported contact to empty strings so as to prevent exceptions.

Private Sub

NormalizeAllContacts(ByRef

ContactArray As Array)

Capitalizes the first letter of the first and last names of all contacts in the contact array.

Private Sub

PrintContactList()

Calls the PrintList method in MyContact (which prints out a a sorted list of all contacts in the database), then raises the ContactsPrinted event.

Private Sub WriteAddToLog

(ByVal ca As

ContactClass.ContactEventArgs)

When the AddedContact event is raised in ContactClass, it is handled by this method, which logs the event and relevant data.

Private Sub WriteDeleteToLog

(ByVal ca As

ContactClass.ContactEventArgs)

When the DeletedContact event is raised in ContactClass it is handled by this method, which logs the event and relevant data.

Private Sub WriteEditToLog

(ByVal ca As

ContactClass.ContactEventArgs)

When the EditedContact event is raised in ContactClass it is handled by this method, which logs the event and relevant data.

Private Sub

WriteNormalizationToLog()

When the ContactsNormalized event is raised in the NormalizeContacts method it is handled by this method, which logs the event and relevant data.

Private Sub

WritePrintingToLog()

When the ContactsPrinted event is raised in the PrintContactList method it is handled by this method, which logs the event and relevant data.

Private Sub

WriteAddErrorToLog(ByVal ca

As

ContactClass.ContactEventArgs)

When the AddError event is raised in ContactClass it is handled by this method, which logs the event and relevant data.

Private Sub

WriteDeleteErrorToLog(ByVal

ca As

ContactClass.ContactEventArgs)

When the DeleteError event is raised in ContactClass it is handled by this method, which logs the event and relevant data.

Private Sub

WriteEditOldErrorToLog(ByVal

ca As

ContactClass.ContactEventArgs)

When theEditOldError event is raised in ContactClass it is handled by this method, which logs the event and relevant data.

Private Sub

WriteEditNewErrorToLog(ByVal

ca As

ContactClass.ContactEventArgs)

When theEditNewError event is raised in ContactClass it is handled by this method, which logs the event and relevant data.

Private Sub

WriteNormalizationErrorToLog

()

When theContactsNormalizedError event is raised in ContactAuto it is handled by this method, which logs the event and relevant data.

Private Sub

WritePrintingErrorToLog()

When theContactsPrintedError event is raised in ContactClass it is handled by this method, which logs the event and relevant data.

 

 

Reason for the Design:

The reason for this design is greater extensibility of the application through Inversion of Control, as discussed in the introduction.

As an example, look here at the AddFromFile method in ContactAuto:

 

Try

            Dim str As New StreamReader("Add.txt")

            Dim line

            Do

                Dim Contact As New ContactObj

                line = str.ReadLine()

                Contact.FirstName = line

                line = str.ReadLine()

                Contact.LastName = line

                line = str.ReadLine()

                Contact.HomePhone = line

                line = str.ReadLine()

                Contact.CellPhone = line

                line = str.ReadLine()

                Contact.WorkPhone = line

                line = str.ReadLine()

                Contact.Email = line

                If (Contact.FirstName <> Nothing) And (Contact.LastName <> Nothing) Then

                    ReplaceNullValues(Contact)

                    MyContact.AddContact(Contact)

                End If

                line = str.ReadLine()

            Loop Until line = Nothing

            str.Close()

        Catch ex As FileNotFoundException

        End Try

        Dim stwa As New StreamWriter("Add.txt")

        stwa.Flush()

        stwa.Close()

 

The programmer could change this to do whatever the programmer wants, because it uses the Inversion of Control Principle. All the ContactClass AddContact method needs to know is that if anything passes in a contact object it should add it to the database. The ContactAuto class can get a contact however it wants, and the ContactClass will add it the same way. The ContactAuto class is calling a method in ContactClass, and thereby controlling it-An external class is controlling the main class!

Now let’s look at the main class’s dependency diagram:

The ContractObj is the main data type used in this sample.  The ContactClass functions independently of the GUI. In addition, no outside forms (Such as the AddContactForm) connect directly to ContactClass. If these forms need to call something in ContactClass, they do it through ContactApp’s instance of ContactClass.

            Inversion of control works well with this program, as the programmer can change ContactAuto, ContactApp, and all the other forms without needing to change much of anything in ContactClass. This makes the application very extensible, as the programmer can tailor the script and forms to match each customer’s specifications without the programmer having to build a new application every time. This ability is one of the fundamental advantages of VSTA, and one of the ways VSTA is a great time- and money-saver.

 


Posted Jun 18 2009, 03:00 PM by BillL
Copyright Summit Software Company, 2008. All rights reserved.