the native behavior of visual foxpro controls is that as long as a control has focus, pressing ctrl + z (or the escape key) undoes any change that has been made since it gained focus. however, once the user moves off the control, the ability to undo changes in that control, is lost. all that can be done at that point is to revert all pending changes to all fields in the selected record. in other words it is an all or nothing ‘undo’.

however, if you are working in most other microsoft windows applications then pressing ctrl+z undoes changes sequentially, in reverse. in other words, the first press of ctrl+z undoes the last change made, the second undoes the last but one and so on. we can easily mimic this behavior in a vfp form by implementing a "memento" pattern.

what is a memento pattern?

the memento pattern addresses the issue of preserving object state. this should not be confused with storing data (e.g. user preferences) but relates specifically to an object whose settings may be changed during its lifetime, but where we need to be able to restore a previous set of values at any time. data that must be preserved beyond an object’s lifetime must be written out to some form of persistent storage (e.g. to a database, an xml data file or an ini file). the key point is that a memento does not persist between instances of its originator.

what are the components of the memento

the memento pattern comprises three components (figure 1):

  • originator          this is the object whose state is to be captured. it is responsible for creating the memento, passing a snapshot of itself to the memento object and restoring its state from the snapshot when necessary.
  • memento           responsible for storing the internal state of the originator. the amount of information being stored depends entirely on the originator but the memento must ensure that the information is protected from access by objects other than the originator
  • caretaker          responsible for instantiating and holding the memento object. the caretaker has no knowledge of, or stake in, the information being stored by the memento. in addition to creating the memento, it must track which memento belongs to which originator

how do we set about building one

in order to deliver undo functionality to a form, we have to ensure that a memento is created each time a user makes a change in a control. however, the great danger with mementos is that they consume system resources. so in order to keep things manageable we only really want to save changes that have been made when the user leaves each control.

so the first thing we have to do is to ensure that we can compare the exit value from an editable control with the original value that it contained when it received focus. that allows us to create the memento only when the exit value differs from the entry value, in other words, when a change has actually been made. to help with this i added a property named “uoldval” and the following code to the gotfocus() of each base class that has a value property:

this.uoldval = this.value

the next step is to determine whether we need to create a memento or not. one possibility would be to add a flag to each control that defines whether we want to record changes. then add code to the lostfocus() to compare the original and current values when this flag is set and to initiate the creation of a memento whenever they differ. however, this would make each control the “originator” for the memento and that would make restoring the data difficult since the pattern requires that the caretaker only allow the originator to access its memento. the consequence of this approach would be that in order to undo a change the user would first have to navigate to the control – not an unreasonable situation but not quite what we want if we are trying to implement a ctrl + z style “undo the last operation”.

so really we want our form to play the role of “originator” and, since the introduction of bindevent() in visual foxpro, we have a simple way of handling this without the necessity of making even more changes to the individual classes. instead we add a property to our root form class (genforms::xfrmstd) to determine whether the form should support multiple undo levels. this could either be a simple ‘true/false’ flag or, as i prefer, a numeric property named “nundosteps” that limits the number of undo steps that the form allows.

in the form’s setup method we check to see whether nundosteps is greater than 0. if so, we use bindevent() to link the lostfocus() event of each control with a uoldval property to the form’s custom “setmemento()” method. the code is very simple, completely generic and so it can go directly in the form's root class:

*** check to see if we need to handle mementos
if thisform.nundosteps > 0
  *** yes, we do, so instantiate the caretaker object here
  thisform.ocaretaker = newobject( "xcaretaker", "basectrl.vcx" )
  *** and then we need to register the controls on the form
  thisform.registercontrols( this )
  *** set the keypreview property
  thisform.keypreview = .t.
  *** and disable menu handling by re-directing ctrl+z
  on key label ctrl+z keyboard "{ctrl+f5}"
endif
this code calls the custom registercontrols() method that is recursive and handles the actual task of binding the controls:

lparameters toobject
local loobject
*** if we have a container, drill down
if inlist( lower( alltrim( toobject.baseclass ) ), ;
  [form], [pageframe], [page], [container], [grid], [column] )
  for each loobject in toobject.objects foxobject
    thisform.registercontrols( loobject )
  endfor
else
  *** use bindevent to setup the form's setmemento()
  *** method as the delegate for the control's lostfocus()
  if pemstatus( toobject, [uoldval], 5 ) and ;
     pemstatus( toobject, [lostfocus], 5 )
     bindevent( toobject, [lostfocus], thisform, [setmemento], 1 )
  endif
endif

note the use of on key label in the setup code above. this is needed because ctrl + z is a system shortcut combination and is normally processed by the menu before it reaches the form. so in order to have our form able to intercept the ctrl+z key combination, we need to re-direct it to a non-system combination (in this case i am using "ctrl+f5") that we can detect with the following code in the keypress() event, which calls the form’s custom getmemento method to retrieve the last memento saved. notice also that, having trapped the original ctrl+z keystroke we need to kill it to prevent the system from ever seeing it – hence the nodefault in the keypress handler code:

lparameters nkeycode, nshiftaltctrl
*** use ctrl+f10 to handle mementos
if nshiftaltctrl = 2 and nkeycode = 98 ;
   and vartype( thisform.ocaretaker ) = "o" and thisform.ocaretaker.count > 0
   *** we are using mementos
   thisform.getmemento()
   *** and eat the keystroke
   nodefault   
endif

so much for the originator. next we need to address the caretaker which, in my example is an instance of a collection class instantiated by the form, and assigned to a form property. the caretaker could, of course, exist at any level – providing that it is accessible to the originator. however, since the functionality in this case is form specific, there is really no need for the caretaker to exist outside of the form itself and, by making the property to which it is assigned "protected" we can ensure compliance with the requirement that only the originator can access the mementos. an additional benefit of this approach is, of course, that when the form is released, any mementos that are associated with it are also destroyed.

one other thing that we do have to handle is the limiting number of undo steps. this is done in the root form class setmemento()  method where the current count is checked and, if the maximum allowed number of levels has been exceeded, the first item is discarded before the new one is added. the code is, yet again, quite straightforward.

local array lacontrols[ 1 ]
local locontrol, lnmaxmems, lomemento, lckey, lcsource
with thisform
  *** get a reference to the control that delegated its lostfocus to this method
  if aevents( lacontrols, 0 ) > 0
    *** get an object reference to the control
    locontrol = lacontrols[1]
    if not ( alltrim( transform( locontrol.value )) == alltrim( transform( locontrol.uoldval)))
      *** the control has changed
      lnmaxmems = .nundosteps
      if .ocaretaker.count = lnmaxmems
        *** remove the first item in the collection
        .ocaretaker.remove( 1 )
      endif
      *** now create the memento
      lomemento = newobject( 'empty' )
      lcsource = strtran( sys(1272, locontrol), thisform.name, 'thisform' )
      addproperty( lomemento, 'osource', lcsource  )
      addproperty( lomemento, 'uoldval', locontrol.uoldval )
      *** and hand it to the caretaker
      .ocaretaker.add( lomemento )
    endif
  endif
endwith
return

we get a reference to the control that fired the method call using aevents(), and then check the control to see if a change was made. if so, we check the memento count, and if necessary remove the oldest (first) item in the collection. all that is left is to create the memento which, in this example consists of the object hierarchy (as returned by sys(1272) with the form name replaced with “thisform”) and the value that the control held prior to the change being made. labeling the item in this way simplifies the task of restoring the value in the getmemento() method, which is the last piece of the code we need.

the getmemento() method retrieves the last memento from the caretaker and uses its content to restore the change.

if vartype( thisform.ocaretaker ) = "o"
  lnlastitem = thisform.ocaretaker.count
  *** get the last item from the collection
  lomemento = thisform.ocaretaker.item( lnlastitem )
  *** and remove it!
  thisform.ocaretaker.remove( lnlastitem )
  *** now just restore the value to the control
  locontrol = evaluate( lomemento.osource )
  locontrol.value = lomemento.uoldval
endif

the only other issue that must be handled relates to the ‘current record’. obviously we need to clear out any mementos that may exist upon change of record, fortunately that is easy to do with a collection – just call it’s remove() method with a value of “–1”! moreover, since our standard implementation for editable forms is that they are always brought up in “view only” mode, this is easily handled by explicitly clearing the collection every time the form’s mode changes to view (i.e. on leaving edit or add).

you will notice that all of this code is contained within the root form class since, if it is not implemented specifically by setting the nundosteps property greater than zero in the instance of the form, it does nothing at all. the only other class required is the collection class (xcaretaker), which is simply an unmodified first level subclass of the vfp collection base class.

the zip file attached to this post includes all the necessary classes, and a sample form (below) using a free table. run the form, put it into edit mode and make a few changes to control values. then press ctrl+z and watch the changes undo themselves…enjoy!

 

One Response to Using a Memento Pattern to Implement CTRL+Z

  • Sanjay Karia says:

    Very helpful one. Thanks for sharing it. I have some doubts
    1. After typing ABCD in address if I press CTRL+Z, the address remains the same i.e. ABCD, but last name has undo effect.

    Did you tab off  the Address field? The code that creates the memento is triggered by the LOSTFOCUS() event so if you hit CTRL+Z while still in a control the last change is to the PREVIOUS control!

    2. How can we have redo CTRL+R after pressed CTRL+Z.

    3. Can I use this in big Edit box. Normally I use editbox for remarks/notes. Can I do undo word by word?

    Thanks again.
    God Bless you.

    I offer the code on this blog “as is”; I am sorry but I do not guarantee it, nor do I provide free programming services, so please don’t ask me. You have the source code, if you need it to do something other than I provided, feel free to modify the code yourself. All I ask is that when you improve on what I have done, you please let me know, thank you  — Andy

Leave a Reply

Your email address will not be published. Required fields are marked *