| JezUK Ltd - MS Agent: Driving the Agent from .NET |
| << Previous | November 2003 | Next >> |
.NET promises all things to all men. To some, it points to a happy future where contented programmers snap together besoke applications which delighted customers swoon over. For me, it offers a quick way to do the way to the easy stuff, and a whole load of pain for everything else.
Bootstrapping the Agent into .NET is straightforward. The Agent is driven through a set of COM interfaces, so generate wrapper assemblies with AxImp. Reference those assembly DLLs and off you go.
For simple stuff, you don't need anything else. However, for anything slightly more exciting, it gets a little awkward because the Agent runs in a single-threaded apartment. This is, in my experience, pretty unusual. If you haven't worked directly with COM stuff before, you probably won't have encountered it. This is why my workchums spent a week swearing and cursing trying to get this to work, before I had one of those strange slow-treacly-brain-churning only-realise-you-knew-it-once-you'd-worked-it-out revelations.
In my application I have something over there (possibly on another machine) talking back and forth to the Agent over here. The over here bit is multithreaded, which means the Agent won't work unless it's hived off into a seperate thread. More than that, it has to be completely isolated in that thread. For .NET Forms you can cheat by launching a Form in one thread, then calling methods on it from other threads[1]. Try that with the Agent, and it'll throw a System.InvalidCastException - "QueryInterface for interface AgentServerObjects.IAgentCharacterEx failed" - all over the place. To get the multithreaded part of your program working with the Agent you need to get friendly with Threads and Events. Here's the outline of what I've done -
public class AgentFace
{
static private string toSpeak_;
static private AutoResetEvent pleaseSpeak_ =
new AutoResetEvent(false);
static private AutoResetEvent pleaseHide_ =
new AutoResetEvent(false);
static AgentFace()
{ // class constructor to kick things off on demand
// isolate the Agent in its own little place
Thread nt = new Thread(new ThreadStart(AgentLoop));
nt.ApartmentState = ApartmentState.STA;
nt.Start();
} // AgentFace
static void AgentLoop()
{
WaitHandle[] handles = {pleaseSpeak_, pleaseHide_};
AgentWrapper = AgentWrapper.LoadAgent("merlin.acs");
while(true)
{
int h = WaitHandle.WaitAny(handles);
switch(h)
{
case 0:
agent.Speak(toSpeak_);
break;
case 1:
agent.Hide();
break;
} // switch ...
} // while(true)
} // AgentLoop
// here's the multithread facing part
public AgentFace() { }
public ~AgentFace() { }
public void Speak(string sayThis)
{
toSpeak_ = sayThis;
pleaseSpeak_.Set();
} // Speak
public void Hide()
{
pleaseHide_.Set();
} // Hide
} // class AgentFace
...
some_other_part_of_the_program_Class
{
private AgentFace agent_ = new AgentFace();
...
public something()
{
startSomeExternalHardware();
while(askTheHardwareIfItsFinished() == false)
Thread.Sleep(500);
shutdownExternalHardware();
agent_.Speak("Your coffee is ready.");
} // something
}
To expose more of the Agent functionality with this approach needs progressively more and more events, and correspondingly more and more cases on the switch. That isn't especially pretty or maintainable, so a nicer approach might be use a single AutoResetEvent together with a little family of command objects.
[1] Like this, for instance
public class BouncyBalloonWrapper
{
ServerForm.Dialogs.BouncyBalloon balloon_ = null;
Thread worker_;
public BouncyBalloonWrapper()
{
worker_ = new Thread(new ThreadStart(this.FormWorker));
worker_.Start();
} // BouncyBalloonWrapper
~BouncyBalloonWrapper()
{
balloon_.Close();
} // ~BouncyBalloonWrapper
private void FormWorker()
{
balloon_ = new ServerForm.Dialogs.BouncyBalloon();
Application.Run(balloon_);
} // FormWorker
public void Display(string str, Point pos, Size ext)
{
balloon_.Display(str, new Rectangle(pos, ext));
} // Display
public void BounceAround(string str, Point pos, Size ext)
{
balloon_.Bounce(new Rectangle(pos, ext));
} // BounceAround
}
On Saturday morning, Hal ... (4)
The Highway Code - Rule 92 (7)
Courgette Cornucopia (5)
The Rainbow Orchid Volume Two. I am excited. Fact. (1)