We had a little XMLNotepad prototype working: we could edit in the window, and when we hit Enter, we got a new section with paired P tags. We hadn't yet put the cursor where we wanted it, between the tags, but we knew how to do that. So we thought we should put the code in better order. Here's what it looked like when we started.
using System;
using System.Drawing;
using System.Windows.Forms;
using NUnit.Framework;
using System.Collections;
using System.Text.RegularExpressions;
namespace Notepad
{
class XMLNotepad : NotepadCloneNoMenu
{
[STAThread]
static void Main(string[] args)
{
Application.Run(new XMLNotepad());
}
public XMLNotepad() {
Text = "XML Notepad";
txtbox.KeyDown += new KeyEventHandler(XMLKeyDownHandler);
txtbox.KeyPress += new KeyPressEventHandler(XMLKeyPressHandler);
}
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;
}
void XMLKeyPressHandler(object objSender, KeyPressEventArgs kea) {
int key = (int) kea.KeyChar;
if (key == (int) Keys.Enter) {
kea.Handled = true;
}
}
void XMLKeyDownHandler(object objSender, KeyEventArgs kea) {
if (kea.KeyCode == Keys.P && kea.Modifiers == Keys.Control) {
txtbox.Text += "controlP";
kea.Handled = true;
}
if (kea.KeyCode == Keys.Enter) {
String[] lines = txtbox.Lines;
Console.WriteLine("LineCount {0}", txtbox.Lines.Length);
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;
Console.WriteLine("LineCount {0}", txtbox.Lines.Length);
}
// debugging keys
if (kea.KeyCode == Keys.L && kea.Modifiers == Keys.Control) {
String[] lines = txtbox.Lines;
foreach ( String s in lines) {
Console.WriteLine(s);
}
kea.Handled = true;
}
if (kea.KeyCode == Keys.S && kea.Modifiers == Keys.Control) {
txtbox.SelectionLength = 0;
kea.Handled = true;
}
if (kea.KeyCode == Keys.Q && kea.Modifiers == Keys.Control) {
Console.WriteLine("cursor: {0}", CursorLine());
kea.Handled = true;
}
}
}
}
What's wrong with that is that all our operational code is in the Form object, in the KeyDownHandler, and we're working directly with the TextBox. Our plan was to create a new object to serve as the "model" in a Model View Controller triad. Our problems turn out to be many. First, we are too new with C# to be sure how (or even whether) to set up MVC in .NET. Second, we aren't facile enough yet with events and delegation to be sure how to proceed.
Take a look at that KeyDownHandler method. It's fielding and decoding the keys typed to see if it likes them, but it is also doing actual functionality of the application. (Most of it now is just debugging and information providing methods, but take a look at Keys.Enter, for example. This code is manipulating the contents of the textbox as follows:
Now, when we looked at this code, we realized that we hadn't expressed our intention in writing it. The code doesn't explicitly say the 7 things above, and that's bad. Worse, however, is that this functionality is all about manipulating the textbox, and not at all about being part of the form. So we knew that we wanted a new object, a TextModel we called it, to start handling the text processing.
An issue that we face is that we can see that the way it works now, the TextModel will know the view, or else the Form will have to rip stuff out of the textbox and jam it into the model, and rip stuff out of the TextModel and jam it into the view. Both of these seem wrong.
It turns out that the answer is easy. Our unfamiliarity with .NET and the way that events work caused us not to see it. A later installment will get rid of this problem, we expect. For now, we're just going to go with it, because we're sure that breaking this function out into the TextModel will help.
We actually fiddled with this a couple of different ways. I'll spare you looking at the false starts. What we finally decided was that our TextModel wanted to think of the text as an array of lines, and that it would be fed the lines by the Form, and have the lines taken back by the Form when processing was done. We realized that every time we called the model, we'd have to give it the lines, and while that is grossly inefficient, we had no measurement that showed it was even noticeable in the real world. So we decided to live with it.
We created an empty TextModel class, just "public class TextModel {}". Then we gave the Form an instance, 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 move it over to the TextModel class. We started by moving over the original control-P code, that just echoed "control P" to the textbox. That was enough to get us going.
Let me emphasize that technique for a moment. Even though, as you'll see in a moment, the real code isn't that tricky, I always like to start a new path with something abysmally simple. If it really is simple, it will only take 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 whether it's putting the model's final contents back in the text box. 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.
Then 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. (Yes, 20 lines without a successful test is a pretty big change!) So we just implemented a stub CursorLines that returned 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);
}
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 that we use it, and recall also that we decided to ignore the efficiency issue and to 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.
Again, a comment on the technique. We're doing a very inefficient and fairly 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: putting things in in 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 article 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 towards smooth, as you have 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 is 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 have 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 every time we pass through. Again, you need to make your own decision, based on experience with the techniques and on your own projects.
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 above, and a corresponding one in the Enter code above, 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;
}
}
As mentioned above, the code isn't expressing intention very well. I'd like to improve that in an upcoming installment. This will also help us hone our skills in C#, which, with only about 8 hours experience, we really need. Further: the connection between our textbox and TextModel is pretty ugly. Thanks to our pals on the XP mailing list, we now have some ideas on how to use events to make this look smoother, and maybe even work better. So watch for that in the future.
This is being written at Lake Okoboji, Iowa, and the next installment won't show up until I get home ... and maybe not for a while after that, as Chet and I sync up our schedules. Please stay tuned!