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
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()
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