IronPython and WPF Part 5: Interactive Console

One of the hallmarks of dynamic language programming is the use of the interactive prompt, otherwise known as the Read-Eval-Print-Loop or REPL. Even though I’m building a WPF client application, I’d still like to have the ability to poke around and even modify the app as it’s running from the command prompt, REPL style.

If you work thru the IronPython Tutorial, there are exercises for interactively building both a WinForms and a WPF application. In both scenarios, you create a dedicated thread to service the UI so it can run while the interactive prompt thread is blocked waiting for user input. However, as we saw in the last part of this series, UI elements in both WinForms and WPF can only be accessed from the thread they are created on. We already know how to marshal calls to the correct UI thread – Dispatcher.Invoke. However, what we need is a way to intercept commands entered on the interactive prompt so we can marshal them to the correct thread before they execute.

Luckily, IronPython provides just such a mechanism: clr module’s SetCommandDispatcher. A command dispatcher is a function hook that gets called for every command the user enters. It receives a single parameter, a delegate representing the command the user entered. In the WPF and WinForms tutorials, you use this function hook to marshal the commands to the right thread to be executed. Here’s the command dispatcher from the WPF tutorial:

def DispatchConsoleCommand(consoleCommand):
    if consoleCommand:
        dispatcher.Invoke(DispatcherPriority.Normal, consoleCommand)

The dispatcher.Invoke call looks kinda like the UIThread decorator from the Background Processing part of this series, doesn’t it?

Quick aside: I looked at using SyncContext here instead of Dispatcher, since I don’t care about propagating a return value back to the interactive console thread. However, SyncContext expects a SendOrPostDelegate, which expects a single object parameter. The delegate passed to the console hook function is an Action with no parameters. I could have built a wrapper function that took a single parameter which it would ignore, but I decided it wasn’t worth it. The more I look at it, the more I believe SyncContext is a good idea with a bad design.

I wrapped all the thread creation and command dispatching into a reusable helper class called InteractiveApp.

class InteractiveApp(object):
  def __init__(self):
    self.evt = AutoResetEvent(False)

    thrd = Thread(ThreadStart(self.thread_start))
    thrd.ApartmentState = ApartmentState.STA
    thrd.IsBackground = True
    thrd.Start()

    self.evt.WaitOne()
    clr.SetCommandDispatcher(self.DispatchConsoleCommand)

  def thread_start(self):
    try:
      self.app = Application()
      self.app.Startup += self.on_startup
      self.app.Run()
    finally:
      clr.SetCommandDispatcher(None)

  def on_startup(self, *args):
    self.dispatcher = Threading.Dispatcher.FromThread(Thread.CurrentThread)
    self.evt.Set()

  def DispatchConsoleCommand(self, consoleCommand):
    if consoleCommand:
        self.dispatcher.Invoke(consoleCommand)

  def __getattr__(self, name):
    return getattr(self.app, name)

The code is pretty self explanatory. The constructor (__init__) creates the UI thread, starts it, waits for it to signal that it’s ready via an AutoResetEvent and then finally sets the command dispatcher. The UI thread creates and runs the WPF application, saves the dispatcher object as a field on the object, then signals that it’s ready. DispatchConsoleCommand is nearly identical to the earlier version, I’ve just made it an instance method instead of a stand-alone function. Finally, I define __getattr__ so that any operations invoked on InteractiveApp are passed thru to the contained WPF Application instance.

In my app.py file, I look to see if the module has been started directly or if it’s been imported into another module. If the module is run directly (aka ‘ipy app.py’) then the global __name__ variable will be ‘__main__’. In that case, we start the application up normally (i.e. without the interactive prompt) by just creating an Application then running it with a Window instance. Otherwise, we are importing this app into another module (typically, the interactive console), so we create an InteractiveApp instance and we create an easy to use run method that can create the instance of the main window.

if __name__ == '__main__':
  app = wpf.Application()
  window1 = MainWin.MainWindow()
  app.Run(window1.root)

else:  
  app = wpf.InteractiveApp()

  def run():
    global mainwin
    mainwin = MainWin.MainWindow()
    mainwin.root.Show()

If you want to run the app interactively, you simply import the app module and call run. Here’s a sample session where I iterate thru the items bound to the first list box. Of course, I can do a variety of other operations I can do such as manipulate the data or create new UI elements.

IronPython 2.0 (2.0.0.0) on .NET 2.0.50727.3053
>>> import app
>>> app.run()
#at this point the app window launches
>>> for i in app.mainwin.allAlbumsListBox.Items:
...     print i.title
...
Harvest Festivals
Mrs. Gardner's Art
Riley's Playdate
August 13
Camp Days
July 14
May Photo Shoot
Summer Play 2006
Lake Washington With The Gellers
Camp Pierson '06
January 28

One small thing to keep in mind: if you exit the command prompt, the UI thread will also exit since it’s marked as a background thread. Also, it looks like you could shut the client down then call run again to restart it, but you can’t. If you shut the client down, the Run method in InteractiveApp.thread_start exits, resets the Command Dispatcher to nothing and the thread terminates. I could fix it so that you could run the app multiple times, but I find I typically only run the app once for a given session anyway.

Comments:

Hi, what about using ThreadPool? See my post: http://gui-at.blogspot.com/2008/06/exploring-test-application-ironpython-2.html I don't see any functional difference and you don't need to care about Invoke.
@lukas, I tried using the ThreadPool instead of manually spinning up a thread and it crashed. At least for WPF, the UI objects must be created on an STA thread and the ThreadPool threads are all MTA. As for not caring about Invoke, this code does not work without the command dispatcher calling Invoke. Just for kicks, I tried commenting it out and I get a "The calling thread cannot access this object because a different thread owns it." if I try to interact with the WPF objects in any way. I see from your blog entry that you're using WinForms not WPF, but I'm 99% sure the rules are the same. From http://msdn.microsoft.com/en-us/library/system.windows.forms.control.invokerequired.aspx: "Controls in Windows Forms are bound to a specific thread and are not thread safe. Therefore, if you are calling a control's method from a different thread, you must use one of the control's invoke methods to marshal the call to the proper thread."
The threading is still kind of mystery for me. Especially when something works for me which shouldn't according to others. Your first point is correct - I use WinForms instead of WPF. I also use IronPython 1.1.1 on .NET 2.0.50727.3053. I do not call methods on WinForms. I only read properties because I simulate all methods via Win32 API calls. For example, when I want to click button, I send mouse click event to the position of the button. Interestingly, when I directly call OnClick method of a button, the method runs OK and the button is clicked. For example, using http://gui-at.cendaweb.cz/GUIAT_PoC.exe run: IronPython 1.1.1 (1.1.1) on .NET 2.0.50727.3053 Copyright (c) Microsoft Corporation. All rights reserved. >>> import clr >>> clr.AddReference('System') >>> clr.AddReference("System.Windows.Forms") >>> from System import * >>> from System.Reflection import * >>> from System.Threading import * >>> from System.Windows.Forms import Application >>> from time import sleep >>> >>> def RunMeCallBack(var): ... global App ... asm = Assembly.LoadFrom('GUIAT_PoC.exe') ... asm_type = asm.GetType('GUIAT_PoC.frmGUIAT') ... App = Activator.CreateInstance(asm_type) ... Application.Run(App) ... >>> App = None >>> ThreadPool.QueueUserWorkItem(WaitCallback(RunMeCallBack)) Wait a moment until the app is loaded and run: >>> App.Controls[2].Text = 'text' >>> App.Controls[0].OnClick(None) The 'text' is added to the listbox. Is this just a lucky chance I have or is it correct behavior? I do not know... PS: I tried it on IronPython 20 RC2 (IronPython 2.0 (2.0.0.0) on .NET 2.0.50727.3053) now and it behaves the same.