When Chet came over today, we decided to work on the actual Notepad. We have a running prototype that fields events, and our plan was to do the story above. It seemed easy enough: our prototype already fields several control characters, which we used to display values from the TextBox control on the console, to figure out what was in the control. (We could perhaps have used tests, as we did for the Regex, but the TextBox was more of a spike to learn to make the GUI code work.)
We talked about how to do it. We have a TextManipulator object that I was playing with, that is set up to wrapper a TextBox control and let us write methods against it. The TextManipulator will become the model for the Notepad, I was thinking. We decided not to use that object, because the code didn't need it, nor did it point in just that direction. So our plan is to work directly in the Windows.Form, right inside the event handlers, until the code tells us what it wants to be like.
This will be good for you. This application is pure GUI. It's nothing but a little text editor, with practically no interesting functionality, at least not yet. So what you'll get to see is how we write the code, test it, refactor it, and even write acceptance tests for our customer (that's us). But that's coming up. Our couple of hours today ... well, you'll see.
We had lots of code for handling events. For example, at random:
void XMLKeyDownHandler(object objSender, KeyEventArgs kea) {
if (kea.KeyCode == Keys.P && kea.Modifiers == Keys.Control) {
txtbox.Text += "controlP";
kea.Handled = true;
}
...
}
The above is the code that handles control-P for our prototype. All it does is add the string "controlP" to the text, when you type control-P. We wrote that a few days ago when we were learning how to handle the keyboard events. As dumb as it looks, I'd like to suggest that it's a good example of a well-chosen "spike". (A "spike" is what we call a simple experiment aimed at learning how to do something. Think of driving a big spike through a board.)
This simple code does at least four valuable things, in just a few lines.
When we're trying to learn how something works, we don't fiddle around trying to get something important implemented. Instead, we take little tiny bites, trying to learn the most while typing the least. Since we don't know what we're doing, by definition, the less code we write, the less we'll confuse ourselves. And we're easily confused when we're trying something we've never done before. Maybe you are, as well.
So, based on that spike, the handling of Enter for our story gave us these simple tasks:
It seemed easy enough. We wrote the handler in the obvious way, referring to kea.KeyCode==Keys.Enter. No problem. Only thing was, we needed the code to get the CursorLine. It turns out that I had written some hack code to figure out which line hand the cursor, so I was going to extract that into a method, but Chet had a better idea. "Just write the CursorLine method to return a constant. That will tell us if the code works. So we had the handler, and a CursorLine method, like this:
if (kea.KeyCode == Keys.Enter) {
String[] lines = txtbox.Lines;
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];
}
txtbox.Lines = newlines;
kea.Handled = true;
}
...
private int CursorLine() { return 3; }
The event handler is pretty straightforward. We get the lines from the TextBox, and get the cursor line. We copy the lines up through the cursor line, then add our two new lines, then copy the rest of the lines. Then we slam the lines back into the TextBox and report that we handled the event.
As I look at that code now, it doesn't express the intention very well. If I have time tonight I'll refactor it and show what would be better ... and in fact it would have been better to write it expressing intention, because it had a few bugs in it ... various off by one errors. But that's not the core of tonight's tale.
Well, it almost worked, but it didn't. What happened after we got the offs by one out was that the text was added in the right place, but there was also an extra newline added. We saw quickly that it must be the newline that would have been added in a normal TextBox. But we're signaling that we handled the key, with that kea.Handled = true. What was up with that?
We fiddled around a bit. We thought maybe there was a line feed coming through, so we wrote a handler for that. We thought maybe the TextBox used KeyUp instead of KeyDown, so we converted to that. And we kept getting that extra newline. At some point we realized that whenever we typed any control character into our original prototype, the "bell" would ring, even when it was one of the characters that we were handling. So that told us that our handlers were really not suppressing the handling of things by the TextBox. That realization didn't solve the problem, but it told us that either we had a generic kind of bug in our code, or that there was something we didn't understand going on.
A bunch of digging through the TextBox "documentation" showed us that there was another event besides KeyUp and KeyDown: KeyPress. It turns out that in their wisdom, Microsoft has this other event that abstracts the idea of key up and down into "this key was pressed", and produces three events for each key action: Down, Press, then Up. Wonderful. And it also turns out that KeyPress doesn't use those nice mnemonic key definitions: it returns a char type. We suppose that's some historical deal, since the Up and Down events are so nicely OO.
Anyway, we wrote a KeyPress event handler and indicated that we had handled the event. It looks like this:
void XMLKeyPressHandler(object objSender, KeyPressEventArgs kea) {
int key = (int) kea.KeyChar;
if (key == (int) Keys.Enter) {
kea.Handled = true;
}
}
We think that's really ugly, and we posted a note on the C# newsgroup for a better way to do it. All the code actually does, though, is indicate to Windows that the Enter has been handled. The result was that the code works as expected: hit Enter and it inserts the two desired lines, after line 3, because CursorLine() always returns 3. So we went back and extracted the real CursorLine() method:.
private int CursorLine() {
String[] lines = txtbox.Lines;
txtbox.SelectionLength = 0;
int start = txtbox.SelectionStart;
int length = 0;
int lineFound = 0;
int lineNr = 0;
foreach ( String s in lines) {
if (length <= start && start <= length+s.Length + 2 ) {
lineFound = lineNr;
}
length += s.Length + 2;
lineNr++;
}
return lineFound;
}
This isn't pretty, but it works. It's looping over the lines, and checking to see if the cursor position is between the beginning and end of each line. The +2 handles the CrLf at the end of each line. We suspect there might be a better algorithm, and cleaner C# code, and we promise to clean it up before we're done. If we don't, you can write us an email and ask why we didn't. But for now it works, so we're going with it.
We were pretty whipped, since we had been going at it for a couple of hours, and had been beating our heads against some unknown API thing for much of it. So we decided to call it a day.
Quite a bit. We have code, rough though it is, to figure out what line the cursor is on. We have code, rough though it is, to compute a new line collection. We have handled the keyboard events, and though we don't like what we had to do, we are now sure that we can do all the control sequences that the application needs to do, at least for the basic editing stories.
We have written some fairly crummy code that needs addressing. But our story isn't done: we still have to set the cursor to the correct new location, task 4. We have only done a couple of hours' work so far, so we feel it's OK that it isn't sweet yet. More important, though, we've been running without tests, other than visual ones. That's rather scary but we didn't know how to do the GUI stuff test first, and we felt it was better to learn how to do what we had to do. We'll rectify the testing problem tomorrow, I expect.
I'd feel much better if we knew how to do today's exercise test first. But we didn't. If you have any ideas, let us know, we'll try it next time.
We'll start cleaning this code up. And we'll show you how to extract this GUI code off to a testable space. And -- watch for this -- we'll do an acceptance test. That's right: an automated acceptance test for a GUI application. Can we do it? Stay tuned ...