Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Creating Undo functionality in a Visual FoxPro TextBox


:P
On this page:

The Visual FoxPro textbox isn’t exactly a highly featured control as is – in Help Builder I had to bend over backwards to make it work as a text based editor for long text that includes formatting. But at the same time I’ve not been able to find a decent replacement either. Most ActiveX based text controls are either buggy as hell (at least in VFP) or they are very, very slow if you hook up any COM event code to the key processing which Help Builder needs to do.

 

For the most part the stock Fox TextBox works fine – except for two things:

 

1 . There’s a bug in the control that causes the control to wrap funky if linefeeds happen to fall into a very specific area in the right margin. This can cause linefeeds to get ‘eaten’ by the text box which bunches up text that can all of a sudden jump when the textbox gets resized or other text gets entered to push the linefeed off a margin. This may seem like a really obscure bug, but if your doing lots of text editing you’ll run into this one pretty quickly. According to Calvin Hsia this one will be fixed in VFP 9.0 SP1

 

2. Undo behavior. Visual FoxPro’s TextBox doesn’t do undo real well – the undo buffer gets blown away whenever there’s any sort of programmatic update to the value. This includes control source binding, setting the value explicitly, changing SelText or even pasting text into the control. It also clears if you tab out of the control and immediately come back to it. All of that is really very limited and non-standard behavior.

 

Until SP1 ships I can’t do anything about #1 but I decided maybe I can rig my own Undo buffering in my custom TextBox control. While talking to Calvin at Southwest Fox I was begging him to put better undo behavior into the textbox, but the undo behavior is deeply rooted in the Fox runtime and changing that behavior would likely break a lot of existing code. So no help here. So, Calvin suggested, write your own…

 

At first I thought – yeah, right. Managing Undo buffers with Fox code is likely going to be pretty slow and very memory intensive, because you basically have to save the entire buffer of the control value as the InteractiveChange and ProgrammaticChange events don’t provide you with information on what’s changed, so there’s no easy way to capture just what’s changed to play back.

 

After some thought I tried it anyway just to see how things would work out and here’s what I ended up with:

 

  • Optional custom Undo behavior
  • Undo that survives, programmatic changes
  • Undo that survives focusing to another control
  • Undo that gets cleared only on Refresh or an explicit clear of the Undo buffer
  • Redo that lets you undo the undo

 

Here’s the control relevant code that deals with the Undo behavior on my TextBox subclass

 

DEFINE CLASS wwhtmleditbox AS editbox

 

 

      OLEDropMode = 1

      FontName = "Tahoma"

      FontSize = 8

      Alignment = 0

      AllowTabs = .T.

      Height = 188

      ScrollBars = 2

      Width = 443

     

      oundobuffer = .NULL.

 

      *-- Last time the UndoBuffer was updated in seconds. Internally used value that keeps every character from getting added to the undo buffer._

      nlastundoupdate = 0

 

      *-- Flag used to disable Programmatic changes to the Undo buffer.

      lundonoprogrammaticchange = .F.

 

      lundotracking = .F.

 

      oredobuffer = .NULL.

 

      Name = "wwhtmleditbox"

 

 

      PROCEDURE Init

            this.oUndoBuffer = CREATEOBJECT("wwNameValueCollection")

            this.oRedoBuffer = CREATEOBJECT("wwNameValueCollection")

      ENDPROC

 

 

      PROCEDURE undo

            IF THIS.lundotracking AND THIS.oUndoBuffer.Count > 0

               THIS.lUndoNoProgrammaticChange = .T.

              

               this.oRedoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)

 

               lcValue = this.oUndoBuffer.aItems[this.oUndoBuffer.Count,2]

 

               IF lcValue = this.Value AND this.oUndoBuffer.Count > 1

                  THIS.Value = this.oUndoBuffer.aItems[this.oUndoBuffer.Count-1,2]

                  this.oUndoBuffer.Remove(this.oUndoBuffer.Count)

               ELSE

                  this.Value = lcValue

               ENDIF

               this.SelStart = VAL(this.oUndoBuffer.aItems[this.oUndoBuffer.Count,1])

               THIS.lUndoNoProgrammaticChange = .F.

               this.oUndoBuffer.Remove(this.oUndoBuffer.Count)

            ENDIF

      ENDPROC

 

 

      PROCEDURE redo

            IF THIS.lundotracking AND THIS.oRedoBuffer.Count > 0

               THIS.lUndoNoProgrammaticChange = .T.

               this.Value = this.oRedoBuffer.aItems[this.oReDoBuffer.Count,2]

               this.SelStart = VAL(this.oRedoBuffer.aItems[this.oRedoBuffer.Count,1])

               THIS.lUndoNoProgrammaticChange = .F.

               this.oRedoBuffer.Remove(this.oRedoBuffer.Count)

            ENDIF

      ENDPROC

 

 

 

 

 

      PROCEDURE KeyPress

            LPARAMETERS nKeyCode, nShiftAltCtrl

 

            *** Don't want ESC to wipe out content of field.

            IF nKeyCode = 27

              *** Eat the key

              NODEFAULT

            ENDIF

 

            *** Ctrl-Z

            IF THIS.lUndoTracking

               IF nKeyCode = 26

                  *** Must check for Ctrl-<- which is also 26

                  DECLARE INTEGER GetKeyState IN WIN32API INTEGER

                  IF GetKeyState(0x25) > -1

                     THIS.Undo()

                     NODEFAULT

                  ENDIF

               ENDIF

 

               *** Redo Ctrl-R

               IF nKeyCode = 18

                  THIS.Redo()

                  NODEFAULT

               ENDIF

            ENDIF

      ENDPROC

 

 

      PROCEDURE ProgrammaticChange

            IF THIS.lUndoTracking AND !THIS.lUndoNoProgrammaticChange

               this.oUndoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)

               this.oRedoBuffer.Clear()

            ENDIF

              

      ENDPROC

 

 

      PROCEDURE InteractiveChange

 

            IF THIS.lUndoTracking

               *** Update only in half second intervals,

               *** so if you type a bunch of words it goes in batch

               IF SECONDS() - THIS.nLastUndoUpdate < 1

                   this.nLastUndoUpdate = SECONDS()

                   RETURN

               ENDIF

 

               *** Only write undo on last word boundary

               IF LASTKEY() = 32 OR LASTKEY() = 13 OR LASTKEY() = 44 OR ;

                  LASTKEY() = 46 OR LASTKEY() = 9

                  this.oUndoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)

                  this.oRedoBuffer.Clear()

                  this.nLastUndoUpdate = SECONDS()

                ENDIF

            ENDIF

      ENDPROC

 

 

      PROCEDURE Refresh

            IF THIS.lUndoTracking

               THIS.oUndobuffer.Clear()

            ENDIF

      ENDPROC

 

ENDDEFINE

 

Note this code has one dependency I’m not including here. I’m using a custom NameValueCollection class which stores name and value in an array. You can change that code to use a real Collection and an object to store the buffer value and the SelStart position.

 

The idea is essentially that every InteractiveChange and ProgrammaticChange is monitored and potentially writes out the value to the UndoBuffer collection. The InteractiveChange is staggered so that it only writes out data if the user is not actively typing and if the cursor is on a word boundary. This reduce the amount of values that get stored tremendously. It looks like other applications like Word use a similar approach although Word’s behavior is a bit different.

Notice also this shitty code:

 

               IF nKeyCode = 26

                  *** Must check for Ctrl-<- which is also 26

                  DECLARE INTEGER GetKeyState IN WIN32API INTEGER

                  IF GetKeyState(0x25) > -1

                     THIS.Undo()

                     NODEFAULT

                  ENDIF

               ENDIF

 

 

In their infinit wisdom somebody decided to map the keycode of 26 to both Ctrl-Z and Ctrl-Left Arrow, so there's no easy way to to tell the character. Instead you have to do another check on the key code to see if it's a the left arrow (0x25). If it is this return -127 or -128. Talk about HACK, but it works. It was kinda fun for a few minutes to have Ctrl-LeftArrow tied to the Undo key - Good thing I decided to build in Redo behavior from the start ...

 

I’ve plugged this into Help Builder now and as I’m working on the Web Connection 5.0 docs I’m heavily using the features with some fairly long topics. So far the behavior is looking good. I don’t see any big performance issues for either memory or anything noticeable during typing.

 

I guess I can check that one of my list of VFP wishlist items I was never going to get


The Voices of Reason


 

Vasim
October 27, 2005

# re: Creating Undo functionality in a Visual FoxPro TextBox

I wonder if you could have used the nShiftAltCtrl parameter to check for Ctrl-LeftArrow instead of the hack?

Rick Strahl
October 27, 2005

# re: Creating Undo functionality in a Visual FoxPro TextBox

Nope because it's Ctrl-LeftArrow and Ctrl-Z - both have the same shiftkey id of 2.


Ana María Bisbé York
November 25, 2005

# re: Creating Undo functionality in a Visual FoxPro TextBox

Thank Rick !!

You can find the Spanish version of this article at (Puede encontrar la versión en Español de este artículo en:)

http://www.portalfox.com/modules.php?op=modload&name=Sections&file=index&req=viewarticle&artid=105

Regards / Saludos,

Ana
www.amby.net
www.PortalFox.com

# DotNetSlackers: Creating Undo functionality in a Visual FoxPro TextBox


TOM
April 27, 2007

# re: Creating Undo functionality in a Visual FoxPro TextBox

bullshit

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024