.NETpad 2025: Rewrite (Premium)

It’s been a while since the last update, sorry. I’m not sure about progress, but work has definitely occurred. A lot of work.

Recapping everything I’ve done is somewhere between impossible and pointless, so I will instead provide a high-level view of the past three weeks, which may explain how I ended up where I’m at now.

I would like to get this new version of .NETpad up on GitHub before it’s fully baked so that anyone who is interested in this project can more easily follow along as I work on this major revision. To get to that point, I would (of course) like the code to be at some minimal level of quality. And so I’ve been using AI tools like GitHub Copilot and the Cursor AI editor to fix specific issues I have, and to examine the code base and provide suggestions. There have been a lot of suggestions, which is both good and bad. And I’ve spent more time than I care to remember just working on what I think of as architectural design; for example, whether it makes sense to use more C# classes to further modularize the code. In short, I’ve gone down so many rabbit holes, but I’ve also made some progress.

There are a few bugs in the code that were bothering me. And because this is such a massive project now, it was confusing figuring out where things were happening. I was always going to rewrite the app before posting it to GitHub, but starting with the public 3.0 code version was tiresome and introduced all kinds of issues of its own. And so I finally decided that I needed to rewrite the app from scratch, starting only with the code needed to display the basic user interface and add the first tab programmatically. And then I’d take it from there, working on adding and then removing tabs and keeping everything up-to-date. In a way, this app would start from the end, and would include basically just the new, tab-related code. All the normal app code to handle the menus, commands, find/replace, settings, and whatever else would wait until the tabs code was in a reasonably good place.

So here we go.

? It’ll be just like starting over

I started with a blank new Visual Studio 2022 WPF project. And then I began blocking out the XAML bits the app would need by copying blocks from the current code base in a separate Visual Studio instance and pasting them into the new app’s code. I removed all the XAML code referencing event handlers, so I could add those one-by-one as needed.

This looks simple enough in Visual Studio’s XAML design view, but it’s actually pretty complicated. It’s all Grid-based, since that makes sense for this type of app. There is an outer grid with two rows, both Grids, one that’s 32 DIPs (device independent pixels) tall for the tabs row, and one that contains the rest of the app UI. The bigger Grid consists of two columns, one for the main app body and one for the settings interface, which is hidden by default and not implemented here for now. The main app body Grid has four rows, for the Grid that contains the menu, the Find/Replace UI (hidden by default but also not implemented here for now), TextBox1 (the main text box), and the Status bar. This won’t make a lot of sense, and I’ve since added back some of the event handlers, but it looks like so.

With this done, I could run the app and basically see that the UI was correct. There was no tab, but I had an “Add new tab” button, the window control buttons, the top-level File, Edit, and View menu items (but no actual menus) and Settings gear, the text box, and the Status bar. None of it did a thing. There was no code behind anything yet.

From there, I added most of the basic window management functionality via event handlers like AppWindow_StateChanged(), AppWindow_KeyDown(), MinimizeButton_Click(), MaximizeButton_Click(), and CloseButton_Click(), plus TabGrid_MouseLeftButtonDown(), which allowed me to control the app normally.

And then it was time to get down to the nitty-gritty.

➕ AddNewTab()

As part of my work over the past three weeks, I had worked up a version of the DocumentTab class that included the functionality that was previously in Tabs.cs, but it was a mess and a lot of what felt like unnecessary work. So I went back to my original design: As before, it has a Tabs.cs, and its contents are not a separate class. It’s part of the MainWindow, just like MainWindow.xaml and MainWindow.xaml.cs. Conceptually, this kind of makes sense to me, since the tabs–TabItem controls–are part of the TabControl that’s part of the MainWindow. But whatever. It’s just easier to do it this way. (Sorry, AI overlords.)

The first thing I added in Tabs.cs was AddNewTab(), of course. This method does what it sounds like it does, and I call it in two places: When the app starts up–that’s what gives it its first tab–and when the user clicks the “Add new tab” button. So I created an empty AddNewTab() method, created a Click event handler for the “Add new tab” button, then called AddNewTab() from there and from the MainWindow() constructor. The other things that will normally happen there–loading the settings and getting the app info will wait for later.

Then it was on to actually write AddNewTab(). As I had done with the XAML for the MainWindow, I copied and pasted blocks of code from my previous app version, thinking about each in turn as I went. I was able to remove a few things that weren’t working (or doing anything useful), and it seems to work properly. I did make one temporary change: When I create a new tab, I changed the default text displayed in the tab from the document name minus the extension (i.e. “Untitled”) to the document name minus the extension plus a space and the index of the current tab (i.e. “Untitled 0”, “Untitled 1”, and so on). I did this so I could easily see which tabs were visible when I later added and removed tabs during testing. (In the past, I tested with loaded text documents, which had unique file names, but file operations are coming later.)

At this point, the app would correctly display a single tab with the text “Untitled 0”.

Then, I could click “Add new tab” and a new tab would appear with the correct index number in its display.

AddNewTab() is one thing, but the app also has to handle the transition between each tab. That is, without any code to handle this, there’s nothing tying the tab to the document it “contains”–they’re disconnected from each other otherwise–or the state of that document. So if the user clicks a different tab, or adds a new tab, or whatever, that content and state just kind of disappears. Or, doesn’t do anything.

So there are helper methods in Tabs.cs to, well, help with that. These include:

  • Tab_GotFocus(), which fires when a TabItem receives the focus, such as when the user selects it (which is obvious), creates a new tab (less obvious at first), or in other circumstances.
  • DisplayTabCloseButtons(), which hides all of the “Close tab buttons” on the non-focused tab and displays the “Close tab buttons” on only the current, focused tab.
  • WriteTabText(), which displays the tab header, meaning the text that appears in the actual tab, typically the document name minus the file extension (but for now with the index number attached too). This seems simple, but it’s some of the more complex code that I wrote for this app.

DisplayTabCloseButtons() and WriteTabText() were pretty much good as they were. But I knew Tab_GotFocus() would need some attention. So that was next.

? Tab_GotFocus()

WPF can be complicated in some ways, and this is a good example. The notion of focus seems obvious enough: A control in an app can get the focus when the user clicks it, tabs into it, or otherwise selects it. And it’s not focused otherwise. But there’s more to it. All kinds of things can cause a control to receive focus, if temporarily or quickly, and it’s not always obvious (to me) why. But the app needs to handle this precisely. When a tab receives the focus, things change. It should display the document that’s associated with that tab. And it needs to keep track of various state variables associated with that document (whether it’s saved, and so on).

WPF is wonky enough that I’ve resorted to creating temporary local variables to hold some of that state at the start of certain event handlers so I can ensure that I’m working with the correct DocumentTab object underlying each tab. And that’s the case with Tab_GotFocus(). When the method starts, I create three local variables and assign them the values of three DocumentTab state properties (TextHasChanged, CaretIndex, and SelectionLength). Here’s why: The next thing I do is put the document associated with that DocumentTab object (stored in its Document property) into the text box (by assigning TextBox1.Text to the value of that Document property). Doing that changes the focus to the text box, but it also assigns the value true to TextHasChanged. Even though the text, ahem, has not changed.

After correcting the value of the DocumentTab’s TextHasChanged property, I change the text carat position and selection using its CaretIndex and SelectionLength property values. And then I call DisplayTabCloseButtons(), which changes the focus yet again. Then I call WriteTabText(), changing the focus again. And then I set the focus on TextBox1, so the user can get back to typing.

How many times was TabGotFocus() called in there? Three? More? It’s hard to say without stepping through the code with the debugger. But it’s several times. It’s a lot of focusing and refocusing, and I’d really like to clean up that code. I will. But first I had a more pressing issue. When I went to implement the closing of a tab, I ran into a related issue. As it turns out, clicking a tab’s “Close tab” button also sets the focus … to the tab. And that can be a problem when you’re also closing tabs. I got into a situation where in some cases clicking a “Close tab” button would close two tabs. For some reason.

? CloseTab()

When the user clicks any “Close tab” button, my inventively named CloseTabEventHandler() event handler is called. Stripped of its document management functionality–the final version of the app will do a test to see whether the document displayed by the current tab needs to be saved and will act accordingly–this event handler does one of two things. If there is one or more other visible (open) tabs, it will called a method called CloseTab() and, based on what happens in there, it will close the tab or not. Otherwise–that is, this is the only visible tab–it will close the app.

From there, I created a blank CloseTab() method–it accepts a TabItem as a parameter–and started adding in the code from the original. CloseTab() does three things:

  • Collapses (hides/closes) the tab
  • Sets the focus on the next visible (open) tab if possible
  • If there are no more visible tabs, the app closes. Otherwise, it calls DisplayTabCloseButtons() to correct the display of the remaining “Close tab” buttons.

This works fine except that this method triggers Tab_GotFocus() one or more times. Tab_GotFocus() fires when it finds the next visible tab. And it fires each time DisplayTabCloseButtons() has to change the display of a “Tab close” button. Somewhere in there was a bug that was sometimes, but not always, causing a “Close tab” button click to close two tabs instead of one.

? Enter AI

At this point, I turned to AI. I could have stepped through this mess line by line using the debugger, but I figured this would be faster. It wasn’t, but after asking Cursor to evaluate the code of this functionally cut-down app and explaining that there was something wrong with the close tab logic, it provided an interesting clue, plus some massive code changes.

“The likely cause of the unreliable closing behavior is in the CloseTabEventHandler,” it told me. “It determines the tab to close using MyTabs.SelectedIndex at the time the handler executes. However, this handler is also attached to the close button’s GotFocus event. This means the close logic can be triggered simply by the button gaining focus, which might happen during tab switching or other UI interactions, causing the SelectedIndex to point to a different tab than the one whose close button was originally interacted with (e.g., clicked).”

Cursor is correct. In AddNewTab(), I had added the following code with a comment to remind me why it was there.

It made some recommendations for fixing this, but I was a little put off by the sheer amount of change (in lines of code) it recommended. And by its warning that making those changes would “introduce some unexpected changes into the Tab_GotFocus method.” It said that, “for simplicity,” it would remove the button visibility logic, which, honestly, I’m kind of proud of. And yeah … that wasn’t happening.

But the ample documentation it provided triggered a memory. I had run into an issue elsewhere in the app where it wasn’t always clear what cascading chain of event handlers were firing or why, and there are now a few spots in the code where I do things like create temporary local variables (as above) or short-circuit an event handler so I can test or do something before continuing. And it occurred to me that, while my code is likely not perfect or even desirable, I could keep my Tab_GotFocus() method as-is and just prevent a “Close tab” button click from doing anything.

This was pretty simple, actually. I just added the following code to the beginning of that event handler.

That is, if the sender (the control that triggered the event handler) is a button–and there’s only one button in each tab, the “Close tab” button–just do nothing and exit Tab_GotFocus(). Otherwise, it will run normally.

And that did it.

Now, I could close tabs arbitrarily and it would work accurately. Problem solved.

This doesn’t mean the code I’ve written is “great” or ideal in any way. I will continue using AI to improve the methods I have, and I will keep adding back functionality until I get to a more complete version of the app that actually works properly. This will take time. There are other bugs I need to address as I do this work. But it feels like I’m on the right path.

For now, what works is tab creation and removal, document and state retention when navigating between tabs, and the correct display of the tab header, which includes not just the (truncated, as necessary) name of the document and a “Close tab” button, but also the correct “Close tab” button: If the document associated with a tab is unsaved, the “Close tab” button changes to a bullet as below, instead of the normal “x.” And each tab retains the correct “Close tab” button image as you cycle between the available tabs.

More soon.

Gain unlimited access to Premium articles.

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.

Tagged with

Share post

Thurrott