by John Moody
Reprinted with permission from Visual Basic Programmer's Journal, 8/98, Volume 8, Issue 9, Copyright 1998, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange
Use VB to create an ActiveX control that extends the power of Microsoft Agent in your applications.
By now you're probably familiar with the ubiquitous animated assistants that populate Microsoft Office. These cartoons let you easily and intuitively interact with Office's help systems. What you might not know is that Microsoft's Web site offers an SDK called Microsoft Agent that enables you to create and manipulate similar assistants from within your VB apps (see Figure 1).
Figure 1: Build an Animated Assistant. The Microsoft Agent SDK, found on the company's Web site, gives you the power to create your own simple desktop assistant. The assistant maintains a list of reminders that you store in a Microsoft Access database file. The Merlin character isn't part of the WinWizard Reminders form, but sits on top of it in its own, windowless form. Merlin roams freely over your desktop, giving the character a wider scope of movement than the constrained and window-imprisoned characters in Microsoft Office. The Microsoft Agent SDK ships with the capable Command and Control speech recognition engine, so you can execute commands by voice or keyboard.
With Microsoft Agent, you can display and control one or more animated characters on the screen. The SDK gives you a choice of either creating your own characters or selecting one of the four ready-made characters (Genie, Merlin, Robbie, or Peedy) that ship with Agent (see Figure 2).
Figure 2:Meet the Agent Lineup. The Agent SDK ships with four separate characters: Genie, Merlin, Robbie, and Peedy. You can also create your own characters.
The MSAgent.ocx provides the basic functionality for the assistants. This control includes a complete object model that lets you execute tasks as simple as displaying a character or as intricate as responding to voice commands. Perhaps the best part about the control is that you can use VB5 or VB6 to extend its usefulness in the same way you would a VB textbox.
In this article, I'll show you how to create and use a VB5-extended version of the Agent control to set up a simple desktop assistant. The assistant maintains a list of reminders stored in a Microsoft Access database file. It's not a complicated task, but it illustrates the basics of using the Agent control, and it includes everything you need to know to augment the control's power, including executing tasks on a desktop.
Before you try this control, make sure you have all the necessary components. You can download the core Agent components and the Lernout & Hauspie TruVoice text-to-speech engine from Microsoft's Agent page at www.microsoft.com/msagent. If you have a good microphone on your computer, you'll want to download the Microsoft Command and Control Engine. This component enables you to use voice commands with your characters (see the sidebar, "To Hear is to Obey: Voice Commands in Microsoft Agent"). You also need at least one character file. The desktop assistant you'll create uses the Merlin character, but feel free to use another character if you prefer. The Agent home page also includes other goodies, such as complete documentation and sample code for using Agent from VB, C++, and Java.
Create a New ProjectAfter downloading and installing the Agent SDK, open a VB5 standard EXE project and select the Microsoft Agent Control 2.0 from the Components list. You see a new icon in your toolbox. The icon is supposed to be a secret agent, but it actually looks more like Humphrey Bogart in Casablanca. The Agent control exposes most of the capability of Microsoft Agent—if Agent can do something, you can probably make it happen with this control.
Drop the Agent control on a form and name it "ctlAgent." The Agent control resizes itself automatically to a small icon. If you run the project at this point, you won't see anything on your form, because Agent doesn't have its own standard user interface. You need to add a few lines of code to see anything:
Public Merlin as IAgentCtlCharacterEx
Sub Form_Load()
Private CharPath as String
CharPath = _
"c:\windows\msagent" _
& " \char\"
ctlAgent.Characters.Load "Merlin", _
CharPath & "merlin.acs"
Set Merlin = _
ctlAgent.Characters("Merlin")
Merlin.Show
End Sub
Sub Form_Unload()
Set Merlin = Nothing
End Sub
This code makes Merlin appear when you run the project, assuming you installed the Merlin character file. If you installed a different character file, you need to change any references from the merlin.acs file to the name of the character file you're using. You'll probably want to change the name of the object to your character name as well.
Note that Merlin, unlike the Office assistants, isn't tied to the window. All Agent characters have their own custom-shaped windows, so the characters appear to float on top of the screen. You should also see a character icon in the Windows system tray—a wizard's hat, in Merlin's case. This icon provides a quick pathway to change Agent settings. You might also see a Commands window—if you have voice recognition installed.
You now know how to make Merlin appear—the next step is to put him to work. Drop a command button on your form, then add code to make Merlin appear with a little trumpet fanfare, a greeting, and a rapid flight to the center of the screen (see Listing 1). Piece of cake, right? Well, not exactly—you do have to overcome one or two challenges.
Listing 1
Sub Command1_Click()
Dim CenterX as Long
Dim CenterY as Long
Merlin.Play "Announce"
Merlin.Speak "Hello! | Hi there! Howdy! | Greetings!"
With Screen
CenterX = Int((.Width / .TwipsPerPixelX) / 2)
CenterY = Int((.Height / .TwipsPerPixelY) / 2)
End With
Merlin.MoveTo CenterX, CenterY
Merlin.Speak "How's that?"
End Sub
Listing 1: Control Your Character. The standard Agent characters can speak, move about the screen, and play a number of animations. Combine these features to make your characters "come alive."
First, some animations have a counterpart that returns the character to the resting position. You should use these corresponding animations to ensure fluidity in your character's movements, because most animations assume the character starts from the resting position. Be sure to check the animation list for your character to see if this applies to you. Second, use the Speak method to provide a list of responses, separated by the vertical bar character. At run time, Agent randomly chooses one of the listed responses for your character to speak. Third, MoveTo and GestureAt expect coordinates in pixels, not the twips you normally use in VB. Use the TwipsPerPixelX and TwipsPerPixelY properties of the VB Screen object to convert VB coordinates to Agent coordinates.
Give Your Agent OrdersThis simple animation illustrates how much control Agent gives you over the output. Agent also gives you robust methods to input information, which enables you to create truly interactive characters—a critical factor. Each character Agent control has a Commands collection that specifies the valid commands for that character. You can trigger each Command object in the collection in multiple ways: by selecting the command in the character's popup menu, by raising the Command event from your application, or by speaking the command into a microphone.
The Add method of the Commands collection enables you to add new commands for an animation (see Listing 2). You must assign a unique string ID to each command you add. You can assign the text that appears in the character's popup menu with the Add command at this stage as well, or you can assign it later using the Caption property of the Command object. Set the Enabled and Visible properties to True, or you won't be able to see and use a given command. Setting the Enabled property to False and the Visible property to True grays out a command in the popup menu at your discretion.
Listing 2
Sub Form_Load()
Private CharPath as String
CharPath = "c:\windows\msagent" _
& " \chars\"
ctlAgent.Characters.Load "Merlin", CharPath & _
"merlin.acs"
Set Merlin = ctlAgent.Characters("Merlin")
Merlin.Show
Merlin.Speak "Hello!"
Merlin.Comm ands.Add "center", _
"Center on Screen", "...center...", True, True
Merlin.Commands.Add "joke", "Tell Joke", _
"...joke...", True, True
Merlin.Commands.Add "magic", "Do Magic", _
"...magic...", True, True
End Sub
Sub ctlAgent_Command(ByVal UserInput as Object)
Select Case UserInput.Name
Case "center":
Dim x as Long, y as Long
x = Int((Screen.Width / Screen.TwipsPerPixelX) / 2)
y = Int(Screen.Height / Screen.TwipsPerPixelY) / 2)
Merlin.MoveTo x, y
Case "joke": Merlin.Speak "Why did the chicken " & _
"cross the road?"
Case "magic":
Merlin.Play "DoMagic1"
Merlin.Speak "Abracadabra!"
Case Else:
Merlin.Play "Confused"
End Select
End Sub
Sub Form_Unload()
Set Merlin = Nothing
End Sub
Listing 2: Give the Agent a Mission. You can make your character respond to user input with only a few lines of code. Use a Select Case statement to capture and handle Command events.
Agent raises the Command event whenever the user selects a command. You can handle this event with a Select Case statement that branches on the Name property of the UserInput object. The Command event passes the UserInput object, which is nothing more than a modified version of the Command object with some special properties useful with voice commands.
Agent can't do everything for you, however. It can't tell you the time automatically, and it doesn't translate the coordinates from twips to pixels. You must add a method to get this kind of information each time you use the Agent control. Fortunately, VB5's capability to build and extend ActiveX controls makes it easy to get around these limitations. Simply wrap the Agent control in a VB5 ActiveX control, then add the capabilities you need.
Adding new power is nice, but you also want to avoid losing the functions you have. You can avoid this dilemma in a couple ways. The hard way is to write Property Let and Property Get statements that point to the corresponding properties of the underlying control and raise matching events for all the control's events. Doing it this way is time-consuming, and the coding is tedious.
A better way to accomplish this task: Use the ActiveX Control Interface Wizard, which ships with VB5 and VB6. I have a love-hate relationship with wizards, but this one solves more problems than it creates. You can use this wizard to do much of the dirty work for you.
Interface Wizard Does the Hard PartStart by creating a new ActiveX Control project in VB5. Name the project AgentProControls and name the control AgentPro. Next, add Agent Control 2.0 to the project, then drop a control onto the Control Designer form. The Control Designer form looks like a form window without a border. Call the Agent control ctlAgent.
You're now ready to use the Control Interface Wizard. Fire it up from the Add-Ins menu, then press Next to get past the introductory screen. The wizard displays every event, property, and method you could ever want (see Figure 3). Remove all the preselected items from the list and add all the events the ctlAgent control exposes. Also add the AudioOutput, CommandsWindow, Connected, PropertySheet, SpeechInput, and Suspended properties to the list. Click on Next to continue, then click on Next again to skip the Custom Properties page.
Figure 3: Avoid Drowning in Properties. Use the ActiveX Control Interface Wizard to choose which properties, methods, and events you want your control to support. The wizard automatically generates the code to map the methods you choose to those of the underlying controls.
To complete the job, use the Shift key to select all the items in the list, then select ctlAgent from the Control combo box. Click on Finish to generate the code. When the Wizard finishes, your control has a series of properties and methods, each of which calls its counterparts on ctlAgent. The Wizard also creates event declarations for your control that are raised when the corresponding ctlAgent event is fired.
Unfortunately, if you now add the control to a standard EXE project's form, you get an error stating that your procedure declarations don't match those of the underlying control. The fix is simple: Put ByVal in front of every parameter in each generated event procedure. For example, find this section of code in your project:
Private Sub _
ctlAgent_IdleComplete( _
CharacterID As String)
Then make this change:
Private Sub _
ctlAgent_IdleComplete( _
ByVal CharacterID As String)
Creating the basic interface is that simple. The next step is to decide what additional functions you'd like to add. Of course, you need to be clear on what you want the character to do before you can decide its additional methods. The Merlin character in the sample app displays reminders. Three methods that make sense for this task are Greet, Dismiss, and MoveRelative. The Greet method causes Merlin to appear, the Dismiss method sends him packing, and the MoveRelative method relocates him to a point relative to the upper-left corner of any visible window you choose.
Common sense dictates that you should attach these methods to the Character object, which handles all of Merlin's functionality. However, because character objects are encapsulated inside the Agent control, you can't change them. Instead, you must extend the character objects by adding class modules that emulate and extend Agent's basic object model.
Add Functionality to CharactersYou make enhancements to the characters in the AgentPro control, rather than to other objects such as AudioOutput or PropertySheet. This enables you to add the new capability with only two new classes. The first new class enhances the Character object (see Listing 3). The second new class replaces the Characters collection with one of your own (see Listing 4). Set the Instancing property for each of these classes to PublicNonCreatable. This restricts other developers from accessing your new objects, except through the control.
Listing 3
Private mCharacterID As String
Private mCharacterFile As String
Private mCharacterObject As IAgentCtlCharacterEx
Property Let ID(newID As String)
'Must unload and reload under new ID if changed
If mCharacterObject Is Nothing Then
mCharacterID = newID
Else
Set mCharacterObject = Nothing
MyControl.AgentObject.Characters.Unload _
CharacterID
MyControl.AgentObject.Characters.Load newID, _
mCharacterFile
mCharacterID = newID
Set mCharacterObject = _
MyControl.AgentObject.Characters(CharacterID)
End If
End Property
Property Get ID() As String
ID = mCharacterID
End Property
Public Function Load(CharacterID As String, LoadKey As _
String)
MyControl.AgentObject.Characters.Load CharacterID, _
LoadKey
mCharacterID = CharacterID
mCharacterFile = LoadKey
Set mCharacterObject = _
MyControl.AgentObject.Characters(CharacterID)
End Function
Private Sub Class_Terminate()
MyControl.AgentObject.Characters.Unload mCharacterID
Set mCharacterObject = Nothing
End Sub
Listing 3: Create the clsCharacter Class. Notice that the class holds a reference to the corresponding Character object provided by the Agent control. This cuts down on access time to the control.
Listing 4
Private mCol As Collection
Public Function Add(ByVal CharacterID As String, ByVal _
CharacterFile As String, ByRef ParentControl As _
SecretAgent) As clsCharacter
'create a new object
Dim objNewMember As clsCharacter
Set objNewMember = New clsCharacter
'Load the character of the underlying control
objNewMember.Load CharacterID, CharacterFile
mCol.Add objNewMember, CharacterID
'return the object created
Set Add = objNewMember
Set objNewMember = Nothing
End Function
Public Property Get Item(CharacterID As String) As _
clsCharacter
Set Item = mCol(CharacterID)
End Property
Public Property Get Count() As Long
Count = mCol.Count
End Property
Public Sub Remove(CharacterID As String)
mCol.Remove vntIndexKey
End Sub
Public Property Get NewEnum() As IUnknown
Set NewEnum = mCol.[_NewEnum]
End Property
Private Sub Class_Initialize()
Set mCol = New Collection
End Sub
Private Sub Class_Terminate()
Set mCol = Nothing
End Sub
Listing 4: The clsCharacters Class. Using a custom collection class adds strong typing of collection members to VB's collection object. The NewEnum property lets your code iterate through collection members using a For Each…Next loop.
Before you implement the classes, you need a way to reference the embedded Agent control on the UserControl. One way to do this is to create a standard module in the project, then add this declaration:
Public MyControl As AgentPro
Next, add this line to the UserControl_Initialize routine of your control:
Set MyControl = Me
This line enables the clsCharacters and clsCharacter classes to use the global MyControl variable to access any of the properties and methods of your control, including the ctlAgent control and its object model.
Note that the clsCharacter object refers to the corresponding Character object of the Agent control. This makes it easier to navigate the object models of the AgentPro and Agent controls every time you need access to the Character object. The NewEnum property of the clsCharacters object enables you to use a For Each...Next loop with your custom collection.
Don't forget to add the properties and methods you need to your new objects, either. For example, you should implement MoveTo, GestureAt, Play, and Speak in the clsCharacter class. Each of these methods should pass through to the corresponding method of the Character object encapsulated by your class.
You still don't have a usable object model at this point, because you haven't meaningfully connected any of your classes to your control. Not to worry—making this connection isn't difficult. Simply add a private declaration to the user control's code to store an instance of the clsCharacters class:
Private mCharacters as clsCharacters
You must instantiate this variable when your control is created (dropped on a form). Add this line to the UserControl_Initialize event:
Set mCharacters = new clsCharacters
Now add a read-only public property to expose your variable to users of the control:
Public Property Get Characters() as _
clsCharacters
Set Characters = mCharacters
End Property
This hides the underlying implementation of your control while exposing your custom character objects to the user.
You now have a custom character object, and it's simple to add your own methods to it. For example, you can implement the Greet method by adding a couple lines of code to the clsCharacter object:
Public Sub Greet()
With mCharacterObject
.Play "Wave"
.Speak "Hello! | Hi there!"
End With
End Sub
Implement the Dismiss method in the same way:
Public Sub Dismiss()
With mCharacterObject
.Speak "Adios!"
.Hide
End With
End Sub
However, you must use the Windows API to implement the MoveRelative command. Simply add the necessary declarations to the standard module (see Listing 5). The GetWindowRect API function returns the rectangle coordinates of any window, given the window's handle. The function in the character class should look like this:
Listing 5
Declare Function GetWindowRect Lib "user32" (ByVal hwnd _
As Long, lpRect As RECT) As Long
Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
Public Function ToPixelsX(twipsX as Long) as Long
ToPixelsX = Int(twipsX / Screen.TwipsPerPixelX * .5)
End Function
Public Function ToPixelsY(twipsY as Long) as Long
ToPixelsY = Int(twipsY / Screen.TwipsPerPixelY * .5)
End Function
Listing 5: Use the Win32 API. GetWindowRect returns a RECT type with the coordinates of any window you specify. Use the ToPixelsX and ToPixelsY functions to convert VB's twips to Win32's pixels.
Public Sub MoveRelative(hWnd As Long, _
xVariance As Long, yVariance As _
Long)
Private WindowRect As RECT, x As Long, _
y As Long
GetWindowRect hWnd, WindowRect
x = WindowRect.Left + xVariance
y = WindowRect.Top + yVariance
mCharacterObject.MoveTo x, y
End Sub
You might want to take into account the dimensions of the character, but that's up to you. Agent and Win32 together give you endless possibilities for controlling a character.
Take a Test DriveThe AgentPro control gives you a lot of power. Now it's time to use the control to create a desktop assistant that maintains a list of reminders you store in a Microsoft Access database file. At the appropriate time, the assistant reads the reminders back to you.
The basic structure of the assistant app is uncomplicated. Dropping the AgentPro control on a form automatically adds a reference to the Microsoft Agent control to your application. When the desktop assistant calls the AgentPro control, the underlying Agent control passes the request to the Agent server. Note that developers must have Microsoft Agent installed to use your control (see the sidebar, "Getting Your License").
First, compile the AgentPro control into an OCX and register it with RegSvr32.exe. Next, start a new standard EXE project in VB. You can do this from VB4, VB5, or VB6, as long as you have a registered version of the AgentPro control on your machine. Use the Components tab to add the AgentPro control to the project. Also add a reference to the latest version of Microsoft Data Access Objects (DAO), so you can communicate with the database file.
Now, crank up Microsoft Access and create a new database file. Add a new table called Reminders and include columns for ID, ShortText, VoiceText, RemindDateTime, and Description (see Table 1). Close Access and go back to your VB project, then use the default Form1 to create the list of reminders. It's not important to examine every detail of setting up the form, but you must keep in mind two issues. First, make sure the AgentPro control is initialized properly when your application starts. Do this by setting the control's Connected property to True. This initiates a connection between the embedded Agent control and the Agent server. You should also load a character into the control's Characters collection. You can show the character at this time or wait until later:
Table 1: Build the Reminders Table. Separating the ShortText and VoiceText columns enables you to exploit special speech tags that vary how your character speaks the reminder.
Column Name | Data Type | Description |
ID | AutoNumber | The primary key identifier for the reminder. |
ShortText | Text | The text the list of reminders displays. |
VoiceText | Text | The text (with added speech tags, if desired) the character speaks when a reminder is triggered. |
RemindDateTime | Date/Time | The date and time the character will alert the user to the reminder. |
Description | Memo | More extensive details for the reminder. |
Private myChar as clsCharacter
Sub Form_Load
AgentPro1.Connected = True
Set myChar = AgentPro1.Characters.Add _
("Merlin", "c:\Program " _
& "Files\Microsoft Agent" _
& "\Characters\merlinsfx.acs")
myChar.Show
myChar.Greet
End Sub
Note that calling the custom-built Greet method is as simple as calling a method that originates on the underlying control. The developer doesn't know (or need to know) which functions are Agent's and which are your own.
Your application also needs to query the database periodically to check for reminders to display to the user. Implement this functionality by adding the query code to the Timer event of VB's standard Timer control. The information from the database is passed to the Character object for it to read when the time arrives for a reminder (see Listing 6).
Listing 6
Sub Timer1_Timer()
Dim Sql As String, RS As Recordset, Another As Boolean
Timer1.Enabled = False
Another = False
Sql = "SELECT * FROM Reminders ORDER BY _
RemindDateTime"
Set RS = DB.OpenRecordset(Sql, dbOpenDynaset)
If RS.RecordCount > 0 Then
RS.MoveFirst
Do While Not RS.EOF
If RS!RemindDateTime < Now() Then
myChar.Play "GetAttention"
myChar.Play "GetAttentionReturn"
myChar.Speak "I have " & IIf( _
Another, "another", "a") _
& " reminder for you."
myChar.Speak RS!VoiceText
Another = True
RS.Delete
If RS.EOF Then Exit Do
End If
RS.MoveNext
Loop
If Another Then
myChar.Speak "That's all the reminders" & _
" I have for you right now."
Me.Data1.UpdateControls
Me.DBGrid1.Refresh
End If
End If
RS.Close
Set RS = Nothing
Timer1.Enabled = True
End Sub
Listing 6: Troll for Reminders. Use a Timer control to check for reminders. Merlin's animations can add a lifelike dimension to the speech-synthesis features.
You now have a desktop assistant that reads reminders at the appointed time. You also have a lot of options for making the desktop assistant more useful. For example, to use voice commands, simply add commands to create new reminders or display the list of reminders. You can also add commands to launch applications or browse Web sites.
Because AgentPro is an ActiveX control, it's easy to enhance the Agent control in different ways. I've shown you how to implement an assistant for reminders, but the commands are rich and extensible enough to employ in countless ways. For example, you could make a character point to interesting items on a window in a program's tutorial or computer-based training application. A talking, animated character adds spice to your apps that standard or even customized dialogs simply can't approach.
To Hear is to Obey: Voice Commands in Microsoft Agent
Most people prefer talking to typing. Allowing users to interact and respond by voice improves tremendously the usefulness of your applications. One of Microsoft Agent's major benefits: It lets you interact with your Agent characters using voice commands. Better yet, it's easy to implement these commands.
You'll need a voice-recognition engine that complies with the Microsoft Speech API specifications to use the voice features. If you don't have such an engine already installed, download the Microsoft Command and Control Engine from the Microsoft Agent home page at http://msdn.microsoft.com/workshop/c-frame.htm#/workshop/imedia/agent/default.asp. You'll also need a good microphone—headset mikes work better than desktop or monitor mikes.
Voice-recognition technology, while much improved, is still in its adolescence. Even the best engines don't recognize everything you say. Agent circumvents this limitation by restricting the set of possible commands to those that you create, along with a few housekeeping commands, such as "Hide <character name>" or "Show Commands Window."
However, you're not lost even if Agent makes a mistake in recognizing a command. The Command event of the Character object displays the command it thinks you said, as well as a Confidence integer that indicates how sure it is. The Command event also displays up to two alternate commands (with a confidence ranking) that might match your request. So, a given voice command might return the Text property "browse c drive" with a Confidence property of 65. It might also show the Alt1Name property "browse d drive" with a Confidence property of 61. You can prompt Agent to confirm the command in your program, providing the alternate as a second choice if the first match is wrong.
One of the factors that make voice recognition so difficult is the natural variation in how people phrase statements and commands. Consider a request for a phone call:
Agent lets you define your commands so this kind of variation isn't a problem. When you add a command to the Commands collection, use the brackets, ellipsis, and the vertical-bar characters in the Voice property. Placing text in brackets makes it optional. The ellipsis is a placeholder for one or more unnecessary words, such as please or OK. Use the vertical bar to define alternate keywords for the command. For example, the Voice phrase "... [me] ... ([phone] call | phone) [me] ..." enables Agent to recognize the proper command if the user says any of the variations listed above. This simple yet powerful feature adds tremendous flexibility to inputting voice commands.
You should define the Caption property for each command and set the Visible property to True if you decide to implement voice commands in your application. This causes a command to show up in the Commands window, prompting the user with a list of all available voice commands. You should also set the Caption and Visible properties, which cause the command list to show up in the popup menu for the character, an important consideration for users who don't have speech-input capabilities on their computers.
Not everyone prefers to use verbal commands over other input methods. Most people are used to pointing, clicking, and dragging, and some won't want to interrupt their work to give a command verbally. Try to provide traditional ways for your users to input information for any voice commands you might choose to implement in a given app.
Getting Your LicenseSo, you're ready to use Microsoft Agent to add some pizzazz to your applications. All you need to do is package the Agent files and ship them with your installation files, right? Well, not quite.
Microsoft includes a single-user license good on your computer only when you download and install the Agent components from its Web site (http://msdn.microsoft.com/workshop/c-frame.htm#/workshop/imedia/agent/default.asp). If you want to distribute applications that use Agent, you need to find a way to get the components installed on your users' PCs.
You can do this in two ways. First, you can write code that automatically downloads and installs the Agent components over the Web from Microsoft's site. However, this requires a lot of needless coding and assumes your users are connected to the Web during the installation.
A better way is to obtain a free redistribution license from Microsoft, which allows you to distribute the Agent components with your application. You can obtain a license by sending e-mail to msagtlic@microsoft.com.
Download the code for this article here