A New Idea


While munching my luncheon chicken strips , I drew the following design picture in my notebook:

click to expand

The last time I did this design thing, as you ll read about in the next chapter, I worked for a few hours over a period of a few days, going through cases and figuring things out in some detail. And I got it wrong, even after all that thinking. This time, a few things popped out right away:

  1. At the time we do the Snapshot(), we can know the character typed. We can get it from the KeyEventArgs that s passed to the KeyPress event.

  2. Since Snapshot() is done by the TextModel, before the typing is completed, we will also know the SelectionStart and SelectionLength. This is certainly enough to let us recognize an insert of an ordinary character, and I think a lot more as well.

  3. Instead of waiting until next time and then optimizing the previous snapshot, let s try to get it right the first time. Whose idea was that optimization thing, anyway? It was hard to implement and hard to understand.

  4. We ll probably wind up using an Interface like the previous one, but I m going to delete the whole thing and start over nonetheless. I want the code to reflect the real need, not a foreseen need, so I ll wait and put it back in. It s only a few lines. (Again a sign that starting over might have been better.)

  5. It should be possible to do this in a test-driven fashion. I can set up a TextModel with whatever values I want, and I can send it a Snapshot() message with parameters indicating what s happening next.

The design sketch is so small because the idea seems much simpler than the last one and because I feel the need to let the tests and code help me, rather than speculate so much. Speculation got me in trouble last time. Let s begin. The general direction I d like to take is to move the Undo- related keyboard analysis into the Snapshot processing instead of leaving it in the KeyDown logic. We start with this:

 internal void XMLKeyDownHandler(object objSender, KeyEventArgs kea) { 
if (kea.KeyCode != Keys.ControlKey &&
kea.KeyCode != Keys.Menu &&
kea.KeyCode != Keys.ShiftKey &&
kea.Modifiers != Keys.Alt &&
!(kea.KeyCode == Keys.Z && kea.Modifiers == Keys.Control)) {
GetText();
model.Snapshot();
}
if (kea.KeyCode == Keys.Enter && kea.Modifiers == Keys.None) {
CallModel(enterAction);
kea.Handled = true;
}
else if (kea.KeyCode == Keys.Enter && kea.Modifiers == Keys.Shift) {
CallModel(shiftEnterAction);
kea.Handled = true;
}
else if (kea.KeyCode == Keys.Z && kea.Modifiers == Keys.Control) {
model.Restore();
PutText(textbox, model.LinesArray(), model.SelectionStart);
kea.Handled = true;
}
}

And we get to this:

 internal void XMLKeyDownHandler(object objSender, KeyEventArgs kea) { 
GetText();
model.KeyboardSnapshot(kea);
if (kea.KeyCode == Keys.Enter && kea.Modifiers == Keys.None) {
CallModel(enterAction);
kea.Handled = true;
}
else if (kea.KeyCode == Keys.Enter && kea.Modifiers == Keys.Shift) {
CallModel(shiftEnterAction);
kea.Handled = true;
}
else if (kea.KeyCode == Keys.Z && kea.Modifiers == Keys.Control) {
model.Restore();
PutText(textbox, model.LinesArray(), model.SelectionStart);
kea.Handled = true;
}
}

and in textmodel.cs:

using System.Windows.Forms;
...
public void KeyboardSnapshot(KeyEventArgs kea) {
if (kea.KeyCode != Keys.ControlKey &&
kea.KeyCode != Keys.Menu &&
kea.KeyCode != Keys.ShiftKey &&
kea.Modifiers != Keys.Alt &&
!(kea.KeyCode == Keys.Z && kea.Modifiers == Keys.Control)) {
this.Snapshot();
}
}

The tests run. I hate that complex if statement. Let s give it some intention :

 public void KeyboardSnapshot(KeyEventArgs kea) { 
if ( ! ShouldSnapshot(kea)) return;
this.Snapshot();
}
private Boolean ShouldSnapshot(KeyEventArgs kea) {
if (kea.KeyCode == Keys.ControlKey) return false;
if (kea.KeyCode == Keys.Menu) return false;
if (kea.KeyCode == Keys.ShiftKey) return false;
if (kea.Modifiers == Keys.Alt ) return false;
if (kea.KeyCode == Keys.Z
&& kea.Modifiers == Keys.Control) return false;
return true;
}

The tests still run. I observe that Snapshot() is now unconditionally doing a full snapshot of the textmodel, so let s rename it to FullSnapshot():

 public void  FullSnapshot  () { 
SavedModels.Push(new TextModel(this));
}

referenced by:
public void KeyboardSnapshot(KeyEventArgs kea) {
if ( ! ShouldSnapshot(kea)) return;
this. FullSnapshot ();
}

and in XMLNotepad.cs:
void MenuInsertTags(object obj, EventArgs ea) {
NotepadMenuItem item = (NotepadMenuItem) obj;
GetText();
model. FullSnapshot ();
model.InsertTags(item.Action);
PutText(textbox, model.LinesArray(), model.SelectionStart);
}

Now we re ready to handle our case: an ordinary keystroke. Let s limit ourselves also to selection length zero.

 public void KeyboardSnapshot(KeyEventArgs kea) { 
if ( ! ShouldSnapshot(kea)) return;
if (SimpleInsertCharacter(kea))
this.SnapshotInsertCharacter();
else
this.FullSnapshot();
}
private Boolean SimpleInsertCharacter(KeyEventArgs kea) {
if (SelectionLength > 0) return false;
if (kea.Modifiers != Keys.None) return false;
char c = (char) kea.KeyValue;
if (Char.IsLetterOrDigit(c)) return true;
if (Char.IsPunctuation(c)) return true;
return false;
}
private void SnapshotInsertCharacter() {
FullSnapshot();
}

All we have done here is break out the case of selection length zero, no modifiers such as control or alt, and letters , digits, and punctuation. (Char doesn t seem to offer an easy way to know whether a graphic is an ordinary character.) This will cover most of our cases anyway, and the learning will be in the next step. In the code so far, of course, we just fall back to the FullSnapshot(). The tests run with FullSnapshot enabled inside SnapshotInsertCharacter(), and they do not if we comment it out. So we know that we re getting there under control of our tests. Now let s make it do something ”we ll build a new class, SingleCharacterSnapshot. Since we never create it with SelectionLength greater than zero, I thought it should just need to know SelectionStart, but here s what I wound up with as a first working version:

 public class SingleCharacterSnapshot : IUndoRestore { 
private int selectionStart;
private int lineNumber;
private String oldLine;
public SingleCharacterSnapshot(TextModel model) {
selectionStart = model.SelectionStart;
lineNumber = model.LineContainingCursor();
oldLine = (string) model.Lines[lineNumber];
}
public void Restore(TextModel model) {
model.Lines[lineNumber] = oldLine;
model.SelectionStart = selectionStart;
}
}

invoked by:
private void SnapshotInsertCharacter() {
SavedModels.Push(new SingleCharacterSnapshot(this));
}

What I wanted to do was just remember the selectionStart, and then inside the Restore() method, get the current line, remove the character right after selectionStart, and put the line back. When I started to do that, it turned out to be a bit tricky. We can get the line containing selectionStart easily enough, but no method in textModel will tell us which character within the line we are interested in. I could have programmed it by intention, but I was more interested in keeping the code working, so I took this intermediate step.

The tests now run...and because I forgot to reset model.SelectionStart in my first implementation, I know that they do not work when I don t have that line in. This tells me that the SingleCharacterSnapshot class is being used and that it works. Now we can make it better.

Lesson  

Now truth is, I can t resist making it better, because I see that we can avoid storing any lines at all by editing the line in place. It s worth noting, however, that this might not be the best idea. First of all, by reducing the storage from all the lines to just one, we have already reduced the storage requirement by a factor of more than 100 for the average file: one line vs. all the lines. Second, while we will save storage by calculating the undo line, we will increase processing time. I m sure this won t be enough to notice, but we should remain aware of it. I ve decided to go ahead, because it should be just a few minutes work. But is this change necessary? Probably not.

Ha! I was mistaken. (First time that ever happened .) I thought there was no method that tells us our position within the caret line, but there is one after all, PositionOfCursorInLine(), which is just what we need to edit the line. Let s change the class to use that:

 public class SingleCharacterSnapshot : IUndoRestore { 
private int selectionStart;
private int lineNumber;
private int positionInLine;
public SingleCharacterSnapshot(TextModel model) {
selectionStart = model.SelectionStart;
lineNumber = model.LineContainingCursor();
positionInLine = model.PositionOfCursorInLine();
}
public void Restore(TextModel model) {
String lineToEdit = (String) model.Lines[lineNumber]; String oldLine = lineToEdit.Remove(positionInLine,1);
model.Lines[lineNumber] = oldLine;
model.SelectionStart = selectionStart;
}
}

Using the PositionInLine() method, we record where the caret is within the selected line. In Restore(), we grab the line from the TextModel, remove one character (the one that must have been typed during the edit), and put the line back. Our tests still run.

Where do we stand? In the most common case (probably over 99 percent of the time), we have reduced the storage requirements of an Undo Snapshot from the entire contents of the file to three integers. This is a good thing.




Extreme Programming Adventures in C#
Javaв„ў EE 5 Tutorial, The (3rd Edition)
ISBN: 735619492
EAN: 2147483647
Year: 2006
Pages: 291

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net