Like many, I had some trouble wrapping my head around validators. So I started experimenting. It took me a bit of back-and-forth between experiment and the documentation, but I think I have the hang of them now.
The validator mechanism provides a hook for checking whether a user-entered value in a control is valid. But it doesn't address the meaning of valid--that is left to the application code (as it should be).
I needed a way to validate the attributes of an object, in a window used for editing various objects of the same class (not all at the same time, of course). And I might have several such windows in the same application, for editing different classes of object. To simplify this problem, I came up with this object-oriented approach to validation for objects.
In particular, I wanted to separate the following responsibilities in the code, all of which are assumed within the description of a wx.Validator:
- 1 Getting and setting values in controls in the GUI. 2 Validating values entered by users in controls in the GUI. 3 Getting and setting values in an object. 4 Translating between representations of values for storage/manipulation and for display.
I have split these responsibilities into the following elements:
DataFormatters, which are aware of data type, and provide methods for translating between display and storage representations (#4) and for validating user-entered values (#2).
- A validator base class, which gets and sets object attributes (#3), dispatches to subclasses for interacting with controls (#1), and optionally invokes a Formatter to translate data representations (#4) and validate user-entered values (#2).
- Validator subclasses, which know how to interact with specific types of controls (#1).
This results in (and from) a somewhat different view of Validators than I have seen heretofore, but I find it works well for me.
What Objects are Involved
This recipe makes use of wx.PyValidator and defines the following validator classes:
ObjectAttrValidator -- Base class for a validator that acts as the intermediary between the attributes of an object and a set of controls. ObjectAttrValidator makes use of optional DataFormatters for translation and validation services. The default behavior of the ObjectAttrValidator (in the absence of a formatter) is to accept any user input as valid, and simply handle two-way transfer of data between an object and the GUI.
ObjectAttrTextValidator -- A specialization of ObjectAttrValidator for interacting with wx.TextCtrls. With DataFormatters, this validator can handle most cases in which the user is expected to type input values.
ObjectAttrSelectorValidator -- Specialization of ObjectAttrValidator for interacting with wx.ControlWithItems widgets (wx.ListBox, wx.Choice, wx.RadioBox). Assumes use of EnumType as described in this recipe in the ActiveState Python Cookbook.
ObjectAttrCheckListBoxValidator -- Validator for CheckListBox controls. (See attached source file ObjectAttrValidator2.py)
ObjectAttrRadioBoxValidator -- Validator for RadioBox controls. (See attached source file ObjectAttrValidator2.py)
This is the base class for object attribute validators. See discussion below listing.
1 import wx 2 3 class ObjectAttrValidator( wx.PyValidator ): 4 def __init__( self, obj, attrName, formatter=None, flRequired=True, validationCB=None ): 5 super(ObjectAttrValidator,self).__init__() 6 7 self.obj = obj 8 self.attrName = attrName 9 self.flRequired = flRequired 10 self.formatter = formatter 11 self.validationCB = validationCB
Often, a field may be left blank, but sometimes must not be blank. Hence the flRequired flag.
Since there are various ways of notifying the user that a validation error has occurred, ObjectAttrValidator uses an optional validation callback for that purpose. This method is called every time validation is called, whether the value is valid or not. This makes it easy for application code to, for example, highlight a control with a bad value, but remove the highlighting when the value has been corrected.
The validation callback should have the following signature:
def validationCB( obj, attrName, value, flRequired, flValid )
def Clone( self ): """ Return a new validator for the same field of the same object. """ return self.__class__( self.obj, self.attrName, self.formatter, self.flRequired, self.validationCB )
Development note: Clone is required for all Validators. Just make sure you include all significant parameters when you clone a Validator. I lost far too much time tracking down bugs when I forgot to add a parameter in the constructor call in Clone.
def SetObject( self, obj ): self.obj = obj
Since my guiding use case was an editor panel used for editing multiple instances of a class (not at the same time, of course), ObjectAttrValidator implements SetObject to allow on-the-fly replacement of the object being edited (or removal, by passing None for obj).
def TransferToWindow( self ): if self.obj == None: # Nothing to do return True # Copy object attribute value to widget val = getattr( self.obj, self.attrName ) if val == None: val = '' if self.formatter: val = self.formatter.format( val ) self._setControlValue( val ) return True
One of the two core routines for transferring data between object and control.
Writing the value to the control is delegated to subclass' _setControlValue method.
TransferToWindow transfers the value from the attribute to the control, and invokes a DataFormatter, if provided, to convert from storage representation to display representation.
def TransferFromWindow( self ): if self.obj == None: # Nothing to do return True # Get widget value val = self._getControlValue() # Check widget value against attribute value; only copy if changed # Get object attribute value oldVal = getattr( self.obj, self.attrName ) if self.formatter: oldVal = self.formatter.format( oldVal ) if val != oldVal: if self.formatter: val = self.formatter.coerce( val ) setattr( self.obj, self.attrName, val ) return True
TransferFromWindow transfers the value from the control to the attribute, and invokes a formatter, if provided, to convert from display representation to storage representation.
Reading the value from the control is delegated to subclass' _getControlValue method.
I use a home-grown object-persistence mechanism in which each object tracks whether it has been changed. To avoid objects being marked dirty unnecessarily, TransferFromWindow checks whether the input value (after optional translation by a formatter) differs from the attribute value before setting the attribute.
Changed 2004.10.13 - Fixed logic bug in Validate method.1
def Validate( self, win ): flValid = True val = self._getControlValue() if self.flRequired and val == '': flValid = False if flValid and self.formatter: flValid = self.formatter.validate( val ) if self.validationCB: self.validationCB( self.obj, self.attrName, val, self.flRequired, flValid ) return flValid
Validate implements the common portions of the validation sequence:
- A blank value is valid if the attribute was not specified as required.
- If no formatter is specified, any non-blank value is valid.
If a formatter is specified, the formatter's validate method is invoked to determine whether the value is valid.
If a validation callback is specified, it is invoked after the value is validated.
Note that the value is in the display representation, not in the storage representation, at this point. The two may be the same, but may not. For example, a date may be stored in ISO format (e.g., 2004-10-02) but presented in "US local" format (e.g., 10-2-2004).
def _setControlValue( self, value ): """ Set the value of the target control. Subclass must implement. """ raise NotImplementedError, 'Subclass must implement _setControlValue' def _getControlValue( self ): """ Return the value from the target control. Subclass must implement. """ raise NotImplementedError, 'Subclass must implement _getControlValue'
These final two methods simply document and enforce the required subclass methods for interacting with controls.
These routines are only responsible for setting and getting the control value. Any data conversion/formatting will be handled by the calling routine.
Implements the required _setControlValue and _getControlValue methods for interacting with wx.TextCtrl widgets. The code is self-explanatory, I think.
ObjectAttrTextValidator, in conjunction with DataFormatters that use regular expressions, can handle most cases that call for users to type input strings, including integer and floating-point numbers, telephone numbers, dates, times, etc.
class ObjectAttrTextValidator( ObjectAttrValidator ): """ Validator for TextCtrl widgets. """ def __init__( self, *args, **kwargs ): super(ObjectAttrTextValidator,self).__init__( *args, **kwargs ) def _setControlValue( self, value ): wgt = self.GetWindow() wgt.SetValue( value ) def _getControlValue( self ): wgt = self.GetWindow() return wgt.GetValue()
Implements the required _setControlValue and _getControlValue methods for interacting with wx.ListBox, wx.ChoiceBox, and wx.RadioBox widgets.
class ObjectAttrSelectorValidator( ObjectAttrValidator ): def __init__( self, obj, attrName, formatter, *args, **kwargs ): super(ObjectAttrSelectorValidator,self).__init__( obj, attrName, formatter, *args, **kwargs )
In this case, the formatter is required. The ObjectAttrSelectorValidator needs the formatter in order to populate the selector.
def _getFieldOptions( self, name ): """ Return list of (id,label) pairs. """ return self.formatter.validValues()
ObjectAttrSelectorValidator Adds the getFieldOptions method, which is not present in the ObjectAttrValidator base class. This method is assumed to return a list of (id, label) pairs. This list is used to populate the selector.
def _setControlValue( self, value ): wgt = self.GetWindow() # Get options (list of (id,value) pairs) options = self._getFieldOptions( self.attrName ) # Sort alphabetically options = [ (opt, opt) for opt in options ] options.sort() options = [ opt for opt in options ] # Replace selector contents wgt.Clear() for id, label in options: wgt.Append( label, id ) # Set selection wgt.SetStringSelection( value )
In addition to setting the value of the control, _setControlValue also populates the selection options of the control. As a result, a call to ctrl.TransferToWindow() results in a fully-populated control.
A variation on ObjectAttrSelectorValidator might implement _setControlValue to retrieve the options from the object, to accomodate a state-dependent set of possible options.
def _getControlValue( self ): wgt = self.GetWindow() return wgt.GetStringSelection()
Other recipes complementary to this one are:
DataFormatters -- Value translation and validation helpers.
EnumType -- Enumerated values referenced by name or number.
Note that while the code imports wx for convenience and clarity, the actual dependency is only on wx.PyValidator.
This implementation of ObjectAttrValidator only does full-value validation. It does not validate partial entries as the user types in characters. Adding this capability should be fairly straightforward, but I haven't had the time to do it yet. If and when I do, I'll post another recipe extending this one.
Depending on the application, it may be preferable to break multiple-element attributes (such as dates, times, and IP addresses) into multiple controls. A multiple-control/multiple formatter version of the ObjectAttrFormatter might be applicable in that case, I think. I have not pursued that issue as yet.
I haven't yet decided whether it would be worthwhile to split out the _setControlValue/_getControlValue operations into another set of helper classes. I have encountered one case in which it would be useful to do so. It would further abstract the validator responsibilities into three logical entities: a formatter to get/set/validate user data values, a control go-between ('facade') to get/set control values, and a validator to use the other two and hook into the wxPython validation/data-transfer mechanism.
The full source file ObjectAttrValidator2.py is attached to this recipe.
The subclasses described above serve as implementation examples for object attribute validators. A few usage examples are provided here.
wx.TextCtrl without validation
Accepts any input string as valid (including blanks and empty string). Transfers unmodified string between an object attribute and a wx.TextCtrl.
person is an instance of class Person.
Class Person defines an attribute firstName.
wgt = wx.TextCtrl( self, -1 ) validator = ObjectAttrTextValidator( person, 'firstName', None, NOT_REQUIRED, self._validationCB ) wgt.SetValidator( validator )
wx.TextCtrl with ISO date validation
Accepts ISO-standard date strings (i.e., YYYY-MM-DD). See DataFormatters for a description of DateFormatter.
person is an instance of class Person.
Class Person defines an attribute activeDate.
wgt = wx.TextCtrl( self, -1 ) validator = ObjectAttrTextValidator( person, 'activeDate', DateFormatter(), False, self._validationCB ) wgt.SetValidator( validator )
person is an instance of class Person.
Class Person defines an attribute status.
- The Person module also defines
Status = EnumType.EnumType( 'Unknown', 'Good', 'Bad' )
wgt = wx.Choice( self, -1 ) validator = ObjectAttrSelectorValidator( person, 'status', EnumFormatter( Person.Status ), True, self._validationCB ) wgt.SetValidator( validator )
A higher level view
My full requirement for an editor window is:
- Provide a single window through which multiple objects of the same class can be edited, one at a time.
- Allow user to select from a list which object to manipulate.
- Validate user-entered values against domain requirements.
- Automatically convert stored values into the necessary representation for presentation to user.
- Automatically convert values entered by user into the necessary representation for storage.
- Automatically update domain objects with (converted) values entered by user.
- Do not allow user to change to a new object if any value entered in a field for a displayed object is invalid.
My application uses a wx.ListCtrl to display a list of people. When a user clicks on a person in the list, the edit panel is updated to display values from the selected person. However, if there was a person already displayed and a value in a control is not valid, the new user is not assigned to the edit panel and the ListCtrl's selection is set back to the previous person. Changes to all edited records are saved when the window is closed or the user issues a Save command.
My full use case is satisfied through the following approach:
- Place the edit widgets on an edit panel.
While constructing the edit panel, record all the edit widgets in a dictionary. I use self._fieldWidgets[fieldName]=wgt.
Implement a SetObject method. In this method, check whether it is okay to change objects before actually changing to the new object:
flDoSwitch = editPanel.Validate() and editPanel.TransferDataFromWindow()
If both Validate() and TransferDataFromWindow() succeed, assign the new object to the edit panel:
if flDoSwitch: editPanel.SetObject( obj ) editPanel.TransferDataToWindow()
In the edit panel, implement a simpler SetObject method to update the validators' object references:
def SetObject( self, obj ): self.obj = obj self._updateValidators() return True def _updateValidators( self ): for name, wgt in self._fieldWidgets.items(): validator = wgt.GetValidator() validator.SetObject( self.obj )
Easy on the brickbats, please.
2004.10.13 - Fixed logic bug in Validate method. Was not processing flRequired if no formatter was assigned. (1)