
I guess this shouldn’t be surprising. But in the course of updating .NETpad for tabs, I’ve experienced challenges that I anticipated and several I did not. You can see this most clearly in the design of the DocumentTab class, which started off with certain assumptions about what I’d need and then evolved over time to address issues I didn’t predict.
Most of those issues are related to what I think of as document state. Managing a single document in a single app window is relatively easy. Managing an unknowable number of documents in a single app window is more difficult, as is displaying each correctly in its own tab as the user navigates back and forth. It seems like there’s always some new issue, waiting around the corner.
The most recent permutation of this type of problem is tied to the location of the text insertion cursor. That is, when you work in a text-based document, there’s a little blinking I-beam (“|”) cursor in the editor that indicates where text will go when you type. In the language of the Windows Presentation Foundation (WPF), that text insertion cursor is called the caret and you can use the TextBox control’s CaretIndex property to get or set the position of the text insertion cursor (as I will continue to call it).
That sounds simple enough. But it is not.
In the current version of .NETpad, the version I posted on GitHub late last year, I only use that CaretIndex property in two places. When the user invokes Go to line, the GoToCommand_Executed() event handler fires, and the first lines of code create a new instance of my GoToLineDialog custom dialog by passing it the line number where the text insertion cursor is, which is calculated using CaretIndex and the TextBox’s GetLineIndexFromCharacterIndex() method.

That’s all it takes to generate the line number shown in the dialog’s text box.

CaretIndex is also used by the ChangePositionText() helper function in Backend.cs, which I call from the TextBox1_TextChanged() event handler. I’ve found in working on the new version of this app that this one place is not sufficient for this purpose, but the point for now is that as the user edits the text in the TextBox, three things happen:
ChangePositionText() looks like so:

In my ape brain, I was thinking of the position of the text insertion cursor as being two values, a line number and a column, because that’s how it’s identified in the UI (which is based on that of Notepad). So in the original DocumentTab class, I included a LineNumber property because I recalled needing this value for Go to line. But as I evolved the app, I realized that I would need more than that. That is, I would have to remember “where” the user was in the text in each document as they moved between tabs. That way, when they returned to a specific document (and tab), they could pick up exactly where they left off. In testing a way to address this need, it occurred to me that I could evolve the LineNumber property into something that stored this location instead. And then it could be used for both purposes.
I wasted so much time on that.
Rather than waste your time, I will just cut to the conclusion and tell you that I do not need a LineNumber property in the DocumentTab class because Go to line doesn’t interact with DocumentTab. It interacts with the TextBox control, and it doesn’t matter which document/tab is visible at the time. These things are not in any way connected. Which I could have seen for myself had I just looked at the code for GoToCommand_Executed(). It accesses the text in the TextBox, not matter what it is, and that hasn’t changed in the tabs-based version of the app.
Before I figured that out, I had transformed the DocumentTab’s LineNumber property into a LineColumn property with two data members, one representing the line number and one representing the column. I could have just added a Column property, but in researching a way to store multiple values in a single variable, I came across the C# tuple, which I was unfamiliar with. Microsoft describes a tuple as lightweight data structure that provides concise syntax for multiple values. And so I made LineColumn as a tuple with two int (integer) values, named LineNumber and Column. Here it is in the default constructor.

After making that change, I had to go and see where LineNumber was referenced in the code so I could change it to accommodate the new tuple. And that’s when I realized my error (well, after hours and hours): As noted, I don’t need to reference a DocumentTab object in any of the places in the code where I am using CaretIndex or calculating the line number and column values for the text insertion cursor. That was all a waste of time.
But not really.
I don’t need my pretty new LineColumn property for Go to line. But I thought I’d need it for the updated version of ChangePositionText(), because that helper method is doing more in the new version of the app. The code from the previous version remains, and works as before. But it has to be updated to save two values tied to the DocumentTab object associated with the current document/tab: The CaretIndex–i.e. the position of the text insertion cursor–and a value that will contain the size of the text selection. That latter value can be 0, meaning no text is selected. Or it can be a value greater than 0 that represents the number of selected characters .
I can’t add that code directly to ChangePositionText(), though I did try that originally. As noted above, ChangePositionText() isn’t the only time this app needs to update the “Ln, Col” and word/character count displays in the status bar. Indeed, this is a fairly obvious bug in the current version of .NETpad that I’ve fixed in the new version. (I will go back and make this change in the current version, too.) So I needed to create a new helper method, which I called SaveCaretPosition(), that will do this work. And then I call it from ChangePositionText() and a few other new event handlers described below.
But first, I had to change DocumentTab. Again.
This one, at least, was easy: I created a new property called SelectionLength that’s a int (integer). And then I added that to the default constructor too, with a default value of 0.

SaveCaretPosition() does three things, after getting the current tab:
It looks like so.

So now I call SaveCaretPosition() at the end of the ChangePositionText() function. That’s nice. But I need to do that more often.
Here’s the bug I addressed.
In the current version of .NETpad, you can start with a new document, tap the Enter key 10 times to move the text insertion cursor down to the 11th line in the TextBox, and when you do, the status bar updates to read “Ln 11, Col 1.”

That’s good, and that happens because ChangePositionText() fires when you do that. But if you then use the Up arrow key to move up, say, 5 lines, the status bar doesn’t update. It still reads, “Ln 11, Col 1” even though the text insertion cursor is somewhere else (“Ln 6, Col 1,” I guess.) And that’s because I am only updating that display when the text changes. That is, ChangePositionText() is only called from TextBox1_TextChanged(). It needs to be called more often than that. It needs to fire when the text insertion moves, too. And the user can do that two basic ways: By pressing an arrow key on the keyboard, or my clicking somewhere in the TextBox with the mouse.
After a bit of research, I determined that I’d have to add two new event handlers to TextBox1: OnKeyUp() and GotMouseCapture(). I did that in the XAML for the main app window:

The code for both is identical: Each calls ChangePositionText(), which now calls SaveCaretPosition() too.

And what that means is that it now works correctly. Using the same example as above, I can open a new document, tap Enter 10 times, and then tap Up arrow 5 times, and the status bar correctly displays “Ln 6, Col 1”.

That’s good. But there’s more.
Fixing that bug felt good, but the underlying issue I was trying to solve was saving the text insertion cursor position information for a DocumentTab.
The question here was whether I needed to just use CaretIndex, or whether I should use my pretty new LineColumn property too. Since LineColumn can be calculated at any time, perhaps I don’t really need to store it as its own property.
Looking through the code, it became obvious. Yes LineColumn can be calculated at anytime. But it’s only calculated once, in ChangePositionText(), which is what creates that “Ln, Col” display in the status bar. So LineColumn was redundant, and all I really need for the text insertion cursor position is CaretIndex. And so I had to bid farewell to the tuple I was so proud of. Sniff. Another assumption down the drain.
Now, I use SaveCaretPosition() to save the text insertion cursor position to the current DocumentTab, and because that function is called during the right events, that’s always up-to-date. When the user switches tabs (and thus documents), the Tab_GotFocus() event handler fires. And one of the things this event handler does now is get the value of the CaretIndex property from the current document (that is, it’s saved text insertion cursor location) and apply it to the TextBox.

This works. It’s hard to see the thin little blinking text insertion cursor to make sure that’s on the right line as you switch between tabs/documents. But it’s in the right place.
With the text insertion cursor position correctly saving to the relevant DocumentTab object, it was time to bring this thing home. That is, there was another state to save, the text selection. If the user selects 1 to whatever number of characters in the TextBox and then switches to another tab, when they return to the first tab, it’s not enough to correctly position the text insertion cursor, that text should still be selected too.
To address that need, I had added a new property to DocumentTab called SelectionLength, as noted above. Now I just needed to use it. There are two places to do so.
The first was already done: I save this value to the current DocumentTab object in SaveCaretPosition().
The second is in Tab_GotFocus(): When the user switches tabs, it will grab the CaretIndex value for the now-current DocumentTab object, as before. But it will also grab the SelectionLength value. And apply that as a text selection after moving the text insertion cursor to the correct location.
This required just one new line of code.

This works nicely. Now, when I select text in a document in one tab, switch to another tab, and then switch back to the first tab, the text insertion cursor is in the right location and the correct text is selected. If no text is selected, then the text insertion cursor just sits there blinking in the correct location.

This selection retention works across all documents and tabs, of course. You can add and remove tabs/documents in any way, make different text selections in each, and that will be retained each time. Nice. Though I still miss my tuple. 🙂
And with that, I had finally completed one of many new document/tab-related tasks on the road to updating this app.
More soon.
With technology shaping our everyday lives, how could we not dig deeper?
Thurrott Premium delivers an honest and thorough perspective about the technologies we use and rely on everyday. Discover deeper content as a Premium member.