Differences between revisions 12 and 13
Revision 12 as of 2004-10-02 18:59:13
Size: 13695
Editor: 64-92-194-161
Comment:
Revision 13 as of 2004-10-02 19:11:24
Size: 14627
Editor: 64-92-194-161
Comment:
Deletions are marked like this. Additions are marked like this.
Line 203: Line 203:
     }}}
In this case, ''the formatter is required''. The `ObjectAttrSelectorValidator` needs the formatter in order to populate the selector.
{{{
Line 258: Line 260:
''person'' is a "Person" object. The "Person" class has an attribute ''firstName''.
Assumptions:
   *
''person'' is an instance of class Person.
 * Class "Person" class defines an attribute ''firstName''.
Line 263: Line 268:
    wgt.SetValidator( validator )
Line 266: Line 272:
Accepts ISO-standard date strings (i.e., YYYY-MM-DD). See DataFormatters for description of `Formatter.DateFormatter`.
''person'' is a
"Person" object. The "Person" class has an attribute ''activeDate''.
Accepts ISO-standard date strings (i.e., YYYY-MM-DD).
See DataFormatters for a description of `DateFormatter`.

Assu
mptions:
   * ''person'' is an instance of class Person.
   * Class
"Person" defines an attribute ''activeDate''.
Line 271: Line 281:
            Formatter.DateFormatter(), False, self._validationCB )             DateFormatter(), False, self._validationCB )
    wgt.SetValidator( validator )
}}}

=== wx.Choice ===
Accepts values as defined in an instance of [[http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/305271 EnumType]].
See DataFormatters for a description of `EnumFormatter`.

Assumptions:
   * ''person'' is an instance of class Person.
   * Class "Person" class 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 )

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 they don'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 (issue 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 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/305271 this recipe in the http://aspn.activestate.com/ASPN/Cookbook/Python/ ActiveState Python Cookbook.

The Classes

ObjectAttrValidator

This is the base class for object attribute validators. See discussion below listing.

Toggle line numbers
   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, val, 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.

    def Validate( self, win ):
        flValid = True
        if self.formatter:
            val = self._getControlValue()
            if val == '':
                if self.flRequired:
                    flValid = False
            else:
                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.

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

ObjectAttrTextValidator

Implements the required _setControlValue and _setControlValue methods for interacting with wx.TextCtrl widgets. The code is self-explanatory, I think.

ObjectAttrTextValidator used 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, 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 _setControlValue 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

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.

Other recipes complementary to this one are:

Code

The full source file ObjectAttrValidator.py ObjectAttrValidator.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:

  • person is an instance of class Person.

  • Class "Person" class 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.

Assumptions:

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

wx.Choice

Accepts values as defined in an instance of http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/305271 EnumType. See DataFormatters for a description of EnumFormatter.

Assumptions:

  • person is an instance of class Person.

  • Class "Person" class 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 )

Comments

Easy on the brickbats, please. ;-)

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.