With a plan in hand ”have a TextModel that is given the lines and has them taken back ”we created an empty TextModel class, just public class TextModel {} . Then we gave the Form a member variable containing a TextModel, with private TextModel model; . And when we initialized the form, we created an instance of TextModel. It looks like this:
class XMLNotepad : NotepadCloneNoMenu
{
private TextModel model;
...
public XMLNotepad() {
Text = "XML Notepad";
textbox.KeyDown += new KeyEventHandler(XMLKeyDownHandler);
textbox.KeyPress += new KeyPressEventHandler(XMLKeyPressHandler);
model = new TextModel();
}
...
}
With that, we re ready to start refactoring the code. The idea is to move functionality out of that big KeyDownHandler method and over to the TextModel class. We started by moving the original Ctrl+P code that just echoed controlP to the Textbox. That was enough to get us going. It looked like this:
void XMLKeyDownHandler(object objSender, KeyEventArgs kea) {
model.Lines = textbox.Lines;
model.SelectionStart = textbox.SelectionStart;
if (kea.KeyCode == Keys.P && kea.Modifiers == Keys.Control) { model.InsertControlPText();
kea.Handled = true;
}
...
textbox.Lines = model.Lines;
textbox.SelectionStart = model.SelectionStart;
}
And the first TextModel class looked like this:
class TextModel {
private String[] lines;
private int selectionStart;
public TextModel() {
}
public void InsertControlPText() {
lines[lines.Length-1] += "ControlP";
}
public String[] Lines {
get {
return lines;
}
set {
lines = value;
}
}
public int SelectionStart {
get {
return selectionStart;
}
set {
selectionStart = value;
}
}
}
Lesson | Let me emphasize that technique for a moment. Even though, as we ll see in a moment, the real code might have been easy enough to write immediately, I always like to start a new path with something abysmally simple. If it really is simple, it will take only a moment to do. And if, as so often happens, it isn t quite as simple as I thought, the learning is isolated to whatever s not right. In this case, what s in question is whether the TextModel and the Form are wired together correctly and whether the Form is properly loading the TextModel with the current Textbox contents and putting the model s final contents back in the Textbox. Doing this in the simple case lets us focus on the central idea of the two objects intercommunicating, without complicating the situation with a complex method inside one of them. However, we have no tests. We re still verifying this code manually. This is very risky, and you ll probably see, as we go forward, that working without tests will often trip us up. Still, we re slow to learn, and sometimes we just can t think of a test or we get too excited about a coding idea to wait. Bad programmers. Go to your room. |
Next we worked on the code we really cared about, the Enter method. We moved it over to the TextModel class as a unit. It doesn t compile because it calls CursorLine. At first we were going to copy CursorLine over as well, but then we realized that was a pretty big change. sb Twenty lines without a successful test is a pretty big change! So we just implemented a stub method for CursorLine(), returning 2. The result would be that when you type Enter, the TextModel would always insert a blank line and a P-tagged line after line 2. Not what we really want, but easy to test. Pretty soon it worked, and we had this method in TextModel:
private ArrayList lines;
private int selectionStart;
public void Enter() {
int cursorLine = CursorLine();
String[] newlines = new String[lines.Length+2];
for(int i = 0; i <= cursorLine; i++) {
newlines[i] = lines[i];
}
newlines[cursorLine+1] = "";
newlines[cursorLine+2] = "<P></P>";
for (int i = cursorLine+1; i < lines.Length; i++) {
newlines[i+2] = lines[i];
}
lines = newlines;
selectionStart = NewSelectionStart(cursorLine + 2);
}
private int CursorLine() {
return 2;
}
With that working (on cursor line 2), we then moved over the CursorLine method, which, completed, looks like this:
private int CursorLine() {
int length = 0;
int lineNr = 0;
foreach ( String s in lines) {
if (length <= selectionStart && selectionStart <= length+s.Length + 2 )
break;
length += s.Length + Environment.NewLine.Length;
lineNr++;
}
return lineNr;
}
Recall that we have to load and unload the TextModel each time we use it, and recall also that we decided to ignore the efficiency issue and do the most straightforward thing. That implied adding a few lines to the KeyDownHandler, so it now looks like this:
void XMLKeyDownHandler(object objSender, KeyEventArgs kea) {
model.Lines = textbox.Lines;
model.SelectionStart = textbox.SelectionStart;
// key down handlers here
textbox.Lines = model.Lines;
textbox.SelectionStart = model.SelectionStart;
}
As you can see, we just rip the lines and selection start out of the Textbox at the beginning of the key down handler and jam them back in at the end. Rough but straightforward.
Lesson | Again, a comment on the technique. We re doing an inefficient and ugly thing here, ripping the guts out of our Textbox and jamming them back. Our mission at this stage is to learn how to build the basic structure of the system, and we re pretty sure that this separate TextModel is the right way to do it, but we don t as yet know the best detailed way to do it. So we re blocking it in: sb we start new features with large bold strokes. We ll clean up the details as we go along. |
Now I m famous for saying that when we say we ll clean it up later, later never comes. And yet, in this chapter we ve recommended putting in code that s clearly not up to our standards of craftsmanship. What s up with that? Are we being inconsistent, or are we taking a risk that things won t get cleaned up? We re sure we re not inconsistent: we re intentionally building from rough toward smooth, as you ve already seen. We put the code in the Form, and now we re moving it to a better place. We ll keep doing that as we go forward. However, there s a risk that we ll leave some bad code lying around. Watch as we go along, and make your own decision on where the balance lies for you. Chet and I have worked together a lot, and we re pretty comfortable with the balance we ve found. We probably will leave some bad code somewhere, but what I predict is that it will be in a place we never visit again during the course of this story. What I hope you ll see is that everything we do visit we make a little better each time we pass through. Again, you need to make your own decision, based on your experience with the techniques and on your own projects.
As you ll see later in the book, we did clean up a lot of code. We never did get a better idea than just saving and restoring all the text and status between the Textbox and our TextModel. I suspect that as long as we stick with using the simple Textbox control, it s the best thing to do.
Now then, we never did set the cursor back where we want it. Maybe you noticed the lines about selection start in the XMLKeyDownHandler method and a corresponding one in the Enter code, where we call NewSelectionStart. We just wrote those methods in a straightforward way, based on a spike we had done earlier. It looks like this:
private int NewSelectionStart(int cursorLine) {
int length = 0;
for (int i = 0; i < cursorLine; i++)
length += lines[i].Length + Environment.NewLine.Length;
return length + 3;
}
public int SelectionStart {
get {
return selectionStart;
}
set {
selectionStart = value;
}
}