Introduction

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:

I have split these responsibilities into the following elements:

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:

The Classes

ObjectAttrValidator

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:

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.

ObjectAttrTextValidator

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()

ObjectAttrSelectorValidator

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[1], opt) for opt in options ]
        options.sort()
        options = [ opt[1] 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()

Notes

Other recipes complementary to this one are:

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.

Code

The full source file ObjectAttrValidator2.py is attached to this recipe.

Examples

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.

Assumptions:

    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.

Assumptions:

    wgt = wx.TextCtrl( self, -1 )
    validator = ObjectAttrTextValidator( person, 'activeDate',
            DateFormatter(), False, self._validationCB )
    wgt.SetValidator( validator )

wx.Choice

Accepts values as defined in an instance of EnumType. See DataFormatters for a description of EnumFormatter.

Assumptions:

        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:

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:

  1. Place the edit widgets on an edit panel.
  2. While constructing the edit panel, record all the edit widgets in a dictionary. I use self._fieldWidgets[fieldName]=wgt.

  3. 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()
  4. If both Validate() and TransferDataFromWindow() succeed, assign the new object to the edit panel:

    •     if flDoSwitch:
              editPanel.SetObject( obj )
              editPanel.TransferDataToWindow()
  5. 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 )

Comments

Easy on the brickbats, please. ;-)

  1. 2004.10.13 - Fixed logic bug in Validate method. Was not processing flRequired if no formatter was assigned. (1)

Validator for Object Attributes (last edited 2008-03-11 10:50:28 by localhost)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.