2005/01/21

Temporarily disabling callbacks in wxPython

A data model can have many views. When a user changes one of the views, that change needs to be reflected in other views. If you use wxPython widgets to implement your views, this can set off an update storm. Here's how to avoid it.

It's frustrating that a programmatic change to a wxPython widget causes callbacks associated with the widget to fire. For example, if you've bound wx.EVT_TEXT in a wxTextCtrl then any subsequent call to SetValue can trigger the bound callback.

In contrast other toolkits (e.g. Apple's Cocoa) recognize that, when you change a widget programmatically, you're usually trying to treat it as just a passive MVC view. You just want to reconfigure your widget, not to generate any callback events. Events are for user input.

wxPython widget classes are derived from wxEvtHandler, and there lies the solution to the problem. If you want to update a widget without triggering its callbacks, first call its SetEvtHandlerEnabled method with an argument of False. Make your update, then call SetEvtHandlerEnabled with an argument of True.

It's a lot of extra typing, but it works. I just wish it hadn't taken me so long to notice wxEvtHandler. I could have avoided a lot of grumbling.

But that sure is a lot of extra typing...

Here's a wrapper function which lets you disable callbacks for the duration of a single method invocation, without suffering the typical verbosity of wxPython:

def withoutCBs(method):

def wrapper(*args, **kw):
widget = method.im_self

oldSetting = widget.GetEvtHandlerEnabled()
widget.SetEvtHandlerEnabled(False)
try:
return method(*args, **kw)
finally:
widget.SetEvtHandlerEnabled(oldSetting)
return wrapper
This wrapper makes it easy to perform a single, callback-free widget update:
    def _uiQuietBtnClicked(self, evt):

withoutCBs(self.field.SetValue)("Quiet")
Of course this is only good for invoking a single method defined directly on the target widget. What if you've composed a complicated method in a controller class, and you need to disable callbacks for the duration of that method? A simple refactoring lets you state explicitly which widget you want to stifle:
def withoutCBs(method, widget=None):

"""Prepare to call method with widget's callbacks
disabled.

If widget is None it is assumed to be the widget
bound to method.
"""
def wrapper(*args, **kw):
w = widget or method.im_self

oldSetting = w.GetEvtHandlerEnabled()
w.SetEvtHandlerEnabled(False)
try:
return method(*args, **kw)
finally:
w.SetEvtHandlerEnabled(oldSetting)
return wrapper
With this modification you can silence a widget for the duration of any operation, regardless of where it's defined:
    def _uiQuietBtnClicked(self, evt):

withoutCBs(self._performComplexOperation,
widget=self.field)()

def _performComplexOperation(self):
...

No comments: