<< PreviousNovember 2003Next >>

Tuesday 11 November, 2003
#MS Agent: Driving the Agent from .NET

.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.

[More about MS Agent]

[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
}

Enter your comment

Name Email
URL
If you have an account, please log in.
No account? Just create one.