hi friends ...

here is a way i used to add an undo - redo functionality to applications.

the basic idea:

  the basic idea i used is that, with every change, i save the code script that will undo it. and save the code script that will redo it in a table that will save these scripts ordered descending from the most recent to the oldest to allow us to undo the most recent change first (logical behavior :-)).
  i used execscript() to execute these codes when needed.
  and when undoing is carried on to some point, making a change next to that will delete the undo-redo scripts that was saved after the last undo point we reach (logical too :-))
  and saving data will erase any previous undo-redo scripts (logical conventional approach !)

  i attached a link to a sample form in which you can try changing the values of different controls: textbox with a character value, textbox with a numeric value, editbox, checkbox, image control, spinner, combobox, optiongroup.
  in addition to that, a grid with a source cursor can be updated, new records can be inserted, and records can be deleted.
all these changes can be undone and redone.

more details:

undoredo table structure: this table is used to save the undo and redo scripts. it is composed of 3 fields:
undo_rank, an integer field that saves the rank of the change, and on which the table is indexed with a descending order so that the most recent changes will be undon first.
un_script and re_script, are memo fields that saves the undo and redo scripts.

saving the undo script:

in this sample i save the undo script temporarily in a custom property in the form. this script will be saved to the undoredo table if a change occur.
for controls with a text entry (textboxes, editboxes, spinners), the
code is saved when the control loses the focus or when the change is committed for other
controls and for the cursor in the grid.

in controls a sample undo script for a control is like this:

text to lcundoscript textmerge noshow
<<sys(1272,this)>>.value='<<this.value>>'
<<sys(1272,this)>>.setfocus
endtext

here the undo script will save the object hierarchy of the control and its value before the change with a setfocus code to that control.
the resultant code (after textmerging the values) is saved in the undoredo table.

an undo script of an insert command will be like this

text to lcundoscript textmerge noshow
delete for it_id=<<lnid>> in tempgrid
<<sys(1272,this)+".parent.grdinvoice.refresh">>
endtext

saving the redo script:

the redo script is saved when the changes are committed, like selecting an option, deleting and inserting records, or at lost focus event after changing the new value in text entry controls.

a sample redo code for that control is like this:

text to lcredoscript textmerge noshow
<<sys(1272,this)>>.value='<<this.value>>'
<<sys(1272,this)>>.setfocus
endtext

a redo script of an insert command will be like this:

text to lcredoscript textmerge noshow
insert into tempgrid values (<<tempgrid.it_id>>,'<<tempgrid.it_name>>',<<tempgrid.it_quan>>)
<<sys(1272,this)+".parent.grdinvoice.refresh">>
<<sys(1272,this)+".parent.grdinvoice.clmitem.setfocus">>
endtext

saving the scripts:

the scripts are saved so that when moving from record to the next in a descending order, the record contains the undo script with the redo script of the next change. this way when executing undo scripts or redo scripts, the record pointer will move so that the correct next script will be executed in the next undo or redo operation. as it is explained below.

executing the scripts:

in the undo commandbutton click() event i put this code:

* running the script to undo

if !empty(undoredo.un_script)

  try
    execscript(undoredo.un_script)
  catch
    messagebox("error in the current undo script or unavailable resources")
  endtry

  skip in undoredo

  thisform.cmdredo.enabled=.t.

  if empty(undoredo.un_script)
    this.enabled=.f.
  endif

endif

the main piece of code is the execscript() function that will execute the undo script. then moving to the next record. with enabling or disabling the redo and undo buttons appropriately.

the redo commandbutton contains a code with a similar idea of the code in the undo commandbutton.

save me button:

it is not a real save 🙂
it is just to perform like the conventional save functions. upon saving, the undo - redo data are dismissed to start a new set of undo redo data.

notes:

  • there are other minor points that could be found while studying the sample.
  • i don't claim that this is the best way of performing undo - redo operations. i think there are other ways that may be more powerful, more safe or easier. there could be saving of data and parameters in a table instead of scripts, or using arrays, or using parameter classes.
  • the provided sample is to clarify my ideas, it is not intended to be error proof or a perfect design.

the sample to download:

http://www.foxite.com/uploads/ccc63abe-5371-4d4b-9ebe-f9c3703eed81.zip

download the zipped sample, unzip and run the main program (undo-redo.prg) and it will find its way to the sample form, table and the sample pictures 🙂

a snapshot of the sample form:

hope it is helpful

ammar hadi ............. iraq
july 2009

One Response to Undo Redo

  • Bhavbhuti says:

    Hi Ammar

    Fine implementation of an undo facility.  Is it possible to SCATTER GATHER the alias in the controlsource to memo/general fields rather than doing it this way?  I have encountered limitations where an combo is using not only the iAccountID but also has joined fields in the view for lookup data like the Account Name, City, etc.  So I was hoping to SCATTER for undo and redo into general / memo fields.  Then when Undo-Redo happens just GATHER the stored state back into the view.

    Please advise.

    Thanks and regards

    Bhavbhuti

    -Hi Bhavbhuti,
    If you are changing values in a cursor related to the combobox then you can use scripts similar to the one I am using in the lost focus of the textbox control of the columns in the grid of the sample form.
    Any how, I can say that there can be many ways to perform the idea. Using Scatter and Gather may end in the same way in that after all you will save the values of the fields of the current record into memo field (no direct way, you need to build a string to hold these values from the scattered memvars) then you need again to transform the data into memvars with similar names to the fields, or create an object and add the needed properties and use the Name clause of Gather.
    I think using Scatter and Gather will add complexity more than the way I did in the sample.
    I want in this blog entry to show the main idea that is when you want to save the undo script of the last change, you need to save the script that will regenerate the original values. The same idea for Redo scripts, save the script that will regenerate the change to be redon. So, for your combobox, try to think in the code you need to excute to redo and undo .. and this way you can write the script inside the Text .. EndText bulk of code to save it. You need to test the code several times till you get the perfect code to save.

Leave a Reply

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