== Introduction == wxPython provides an excellent and growing collection of controls, with means for sizing, aligning and otherwise manipulating them. When one builds an application that populates controls with data from a database one needs to make the controls data-aware, which is to say, one needs to make the controls capable of responding to database state changes and also of responding to requests from the database about whether a database field value should be changed, for instance. At the same time, the database needs to be able to update its content from the controls, at the discretion of the user. This recipe shows a simple way of using a mixin to make at least two of the more commonly used wxPython controls data-aware. It also shows how to decorate a COM object representing a Microsoft Access Recordset to co-operate with these decorated controls. == What Objects are Involved == The recipe involves no unusual Python objects, only wxPython controls such as `wxTextCtrl` and `wxButton`. == Process Overview == Two main pieces are involved, DerivedDBControl and DBMixin. DerivedDBControl is a procedure that accepts a wxPython control class such as `wxTextCtrl`, and arranges to create a class which derives from the wxPython class and class DBMixin, as a default. DBMixin provides storage for the identity of the source field upon whose contents the control is to operate, along with the data source that persists that field. The data source will usually be a database cursor or similar item. In this example, however, for the sake of simplicity, the data source is just an MS Access table. In the code sample line, `DBwxTextCtrl`=DerivedDBControl(`wxTextCtrl`) the class `DBwxTextCtrl` is derived from `wxTextCtrl` and DBMixin. This implies that this class has all of the attributes of the original wxPython class. In this code, nameEdit is an instance of the derived class that is created in exactly the same way that one would create an instance of `wxTextCtrl`. To connect the derived class to a datasourc its two properties, source and fieldName, are set appropriately. The `Update` button directs the datasource to interrogate its listeners, ie, the decorated controls, to provide their contents, so that these data can be used to update a database row, which is then put back into the database. The `Next` button directs the datasource to select the next row of the database, and to ask each of its listeners to update its contents from the newly selected record. == Concerns == * To find a better abstraction for a "datasource". * The datasource gives incomplete access to ADO features, and it probably differs from other Python implementations of wrappers for this standard. == Code Sample == As the reader will see, the following is a complete script that illustrates how to use this idea to connect a wxPython control with a simple data "source". {{{ #!python from wxPython . wx import * class DBMixin : def __init__ ( self ): self . _source = None self . _fieldName = None def __getattr__ ( self, attr ) : if attr == 'source' : return self . _getSource ( ) elif attr == 'fieldName' : return self . _getFieldName ( ) elif attr == 'value': return self . GetValue ( ) else : raise AttributeError, attr def __setattr__ ( self, attr, value ) : if attr == 'source' : self . _setSource ( value ) elif attr == 'fieldName' : self . _setFieldName ( value ) elif attr == 'value' : self . SetValue ( value ) else : self . __dict__ [ attr ] = value def _setSource ( self, _source ): self . _source = _source _source . addListener ( self ) self . tell ( ) def _getSource ( self ): return self . _source def _setFieldName ( self, _fieldName ): self . _fieldName = _fieldName self . tell ( ) def _getFieldName ( self ): return self . _fieldName def tell ( self ): if self . _fieldName and self . _source : self . value = self . _source . getFieldValue ( self . _fieldName ) def changed ( self ): return self . value != self . _source . getFieldValue ( self . _fieldName ) def DerivedDBControl ( wx_control, mixin = DBMixin, tell = None ): class result ( wx_control, mixin ): def __init__ ( self, *args, **kwargs ): wx_control . __init__ ( self, * args, ** kwargs ) mixin . __init__ ( self ) self . id = self . GetId ( ) self . wx_control = wx_control if tell: setattr ( result, 'tell', tell ) return result DBwxTextCtrl = DerivedDBControl ( wxTextCtrl ) DBwxCheckBox = DerivedDBControl ( wxCheckBox ) class DataSource : def __init__ ( self, table ) : self . __dict__ [ 'table'] = table self . __dict__ [ 'listeners' ] = [ ] self . __dict__ [ 'fieldNames' ] = [ Field . Name for Field in table . Fields ] def __getattr__ ( self, attr ) : return getattr ( self . table, attr ) def __setattr__ ( self, attr, value ) : return setattr ( self . table, attr, value ) def addListener ( self, listener ): self . listeners . append ( listener ) def anyChanged ( self ) : return any ( listener . changed ( ) for listener in self . listeners ) def updateFieldValues ( self ) : for listener in self . listeners : self . setFieldValue ( listener . fieldName, listener . value ) def tellListenersUpdate ( self ) : for listener in self . listeners : listener . tell ( ) def setFieldValue ( self, fieldSelector, value ) : if type ( fieldSelector ) is types . IntType : self . Fields [ fieldSelector ] . Value = value elif type ( fieldSelector ) is types . StringType : self . Fields [ self . fieldNames . index ( fieldSelector ) ] . Value = value else : raise AttributeError, fieldSelector def getFieldValue ( self, fieldSelector ): if type ( fieldSelector ) is types . IntType : return self . Fields [ fieldSelector ] . Value elif type ( fieldSelector ) is types . StringType : return self . Fields [ self . fieldNames . index ( fieldSelector ) ] . Value else : raise AttributeError, fieldSelector def MoveFirst ( self ) : self . table . MoveFirst ( ) self . tellListenersUpdate ( ) def MoveLast ( self ) : self . table . MoveLast ( ) self . tellListenersUpdate ( ) def MoveNext ( self ) : self . table . MoveNext ( ) self . tellListenersUpdate ( ) if __name__ == "__main__": class mainWindow ( wxFrame ): def __init__ ( self, parent, id, title, datasource ): wxFrame . __init__ ( self, parent, -1, title, size = ( 300, 400 ) , style = wxDEFAULT_FRAME_STYLE ) self . mainPanel = wxPanel ( self, -1 ) self . mainPanel . SetAutoLayout ( True ) self . datasource = datasource self . nameEdit = DBwxTextCtrl ( self . mainPanel, -1, "", ( 10, 10 ) ) self . nameEdit . source = self . datasource self . nameEdit . fieldName = 'NAME' self . addressEdit = DBwxTextCtrl ( self . mainPanel, -1, "", ( 10, 40 ) ) self . addressEdit . source = self . datasource self . addressEdit . fieldName = 'ADDRESS' self . frenchCB = DBwxCheckBox ( self . mainPanel, -1, "French?", ( 10, 70 ) ) self . frenchCB . source = self . datasource self . frenchCB . fieldName = 'FRENCH' self . saveButton = wxButton ( self . mainPanel, -1, "Update", ( 210, 10 ) ) EVT_BUTTON ( self, self . saveButton . GetId ( ), self . handleUpdate ) self . changeButton = wxButton ( self . mainPanel, -1, "Next", ( 210, 40 ) ) EVT_BUTTON ( self, self . changeButton . GetId ( ), self . handleNext ) self . Show ( True ) def handleUpdate ( self, event ): if self . datasource . anyChanged ( ): self . datasource . updateFieldValues ( ) self . datasource . Update ( ) def handleNext ( self, event ) : if self . datasource . anyChanged ( ) : # ask here whether the user wishes to save the altered record before proceeding # and act accordingly pass if self . datasource . AbsolutePosition < self . datasource . RecordCount : self . datasource . MoveNext ( ) else : self . datasource . MoveFirst ( ) from win32com . client import Dispatch conn = Dispatch ( r'ADODB.Connection' ) connString = r'PROVIDER=Microsoft.Jet.OLEDB.4.0;DATA SOURCE=%s' % r'N:\FWOTS\ZODB\vss.mdb' conn.Open ( connString ) rs = Dispatch ( r'ADODB.Recordset' ) rs.Open ( "[Table]", conn, 1, 3 ) datasource = DataSource ( rs ) app = wxPySimpleApp ( ) frame = mainWindow ( None, -1, "Recipe", datasource ) frame . Show ( ) app . MainLoop ( ) }}} === Comments === Any comments, please contact [[Bill Bell]].