.NETpad 2025: Building Out the Tabs (Premium)

Tabs UI is coming together

Supporting tabs adds complexity to .NETpad, but I’ve successfully merged my backend code for that with a dynamic layout for the front-end. And fingers crossed, but it seems to be working as I push through all the commands and other methods that need to be updated. There’s a lot to do, but this is an ideal time for some code refactoring and code quality improvements, and so I’m taking that on as well. Adding to the complexity, I’ve worked on so many different versions of this app over so much time that I’ve lost track of some of the changes I’ve made. But this was never going to be easy.

In the good news department, my initial bet on easing into this design seems to be paying off. As you may recall, towards the end of the previous phase of this app modernization, I added a major graphical change in the form of a customized title bar area and a basic code-based foundation for multiple documents in the form of an early version of the DocumentTab class (which I’ve subsequently updated, as expected). My hope was that these changes would make the transition to the coming tabs-based UI and multiple document support easier.

As described in the next two sections, my hopes were largely realized: Thanks to the UI changes, replacing the original title bar design with one that supports tabs was relatively straightforward (once I spent a few months determining what compromises WPF would force me to make), and the basic support for DocumentTab in code helped too, though the complexity of managing multiple documents was (and is) still daunting. There’s just so much to keep track of.

So let’s hit on both of these areas.

Updating the app UI for tabs

In .NETpad 3.0 for Windows 11, I created a custom title bar area for the app in anticipation of building out the tabs-based UI that I’m now implementing. Basically, it’s a top Row in the Grid that forms the foundation for the main app window’s user interface that consists of 7 Columns from left-to-right: A Back button (which is hidden in that window’s UI by default and displayed when the settings UI appears), an app icon, the title text, a variable-sized Separator, and the three window control buttons (Minimize, Maximize/Restore, and Close).

I struggled to create the tabs-based update to this for a lot of reasons, the biggest being limitations in WPF. But I described where I finally landed in the previous article, .NETpad 2025: Finalizing the Tabs Layout (Premium), and I’m sure I’ll update the design as needed as I work through all the code changes hinted at below.

The big thing here is that there’s a TabControl in there called MyTabs, but there are no TabItems (tabs) defined in XAML. Instead, that all happens in C# code, dynamically.

And … so far, so good.

Adapt the code to accommodate multiple documents (and tabs)

Previous to the release of .NETpad 3.0 for Windows 11 in late 2024, I used a series of what I think of as global variables to track document state information. For example, a Boolean variable named TextHasChanged was used to determine whether the current (and only) “document”–the text in the main app window’s TextBox–had changed since it was last saved. But knowing that I would be supporting multiple documents (and corresponding multiple tabs), I created a DocumentTab class with multiple properties representing that document state information, and one of those properties, of course, is named TextHasChanged (and is Boolean). And so instead of using global variables, I create a new DocumentTab object when .NETpad 3.0 starts up and add it to a C# List of DocumentTab objects called dts (for DocumentTabs, I guess). And that means I can reference that new runtime object as dts[0], because C# Lists are 0-based like God intended.

With that in place, I did a Copy/Replace across all the documents in the Visual Studio project and replaced each instance of TextHasChanged with dts[0].TextHasChanged. And while I wasn’t 100 percent sure how I would change it in the future, the idea was that the 0 in there would change to some variable–like CurrentTabNumber that would represent not the first DocumentTab in dts but rather the current DocumentTab. That is, dts[0].TextHasChanged would change to dts[CurrentTabNumber].TextHasChanged or similar.

Two months later, I can tell you that that’s exactly what happened. I went back and forth on various methods for tracking the open tabs (and thus also the documents the represent), and while I am sure there are more sophisticated ways to do what I’m doing, I landed on something that at least made sense to my brain.

There are three big high-level coding changes.

First, I have evolved DocumentTab based on my experiences actually using it with multiple documents. I had originally created two constructors for this class, one that supplied default values for all the contained properties, and one that allowed you to pass in the values for the TextHasChanged, DocumentIsSaved, and DocumentName properties. But I am now using a third and preferred constructor that is similar to the first noted above but has a single required parameter for a TabItem that is passed into the constructor, creating an association between that UI control and the data associated with it. That looks like so.

Second, I created a Tabs.cs file that is similar to Backend.cs but contains all the helper methods for tabs. The key one being AddNewTab(), which contains the code for dynamically creating a new WPF TabItem object at runtime. And that’s the third major high-level coding change: Creating a complex WPF UI object like the highly customized TabItem I need using C# is difficult.

Tabs.cs contains a variety of methods, some obvious (CloseTab(), GetCurrentTabItem(), and so on) and some not so obvious. For example, to emulate how Notepad works where you can only see (and thus click) the Close tab button for the currently selected tab, I had to write two methods: HideAllCloseTabButtons() to hide the Close tab button on every visible tab, plus DisplayTabCloseButton() to make the Close tab button visible only on the currently-displayed (selected) tab.

Stepping through each core function in the app, I found that some things needed major rewrites to accommodate multiple tabs, while others didn’t.

Consider the first command in the app, New. This is what happens when the user clicks File > New or types Ctrl + N. In the previous one-document version of the app, this command (NewCommand_Executed()) resulted in a simple if-else loop that checked whether the text in the document had changed. If it had, the user would be prompted with a Save as dialog. If it hadn’t, it would run a helper method called NewDocument(). And that method couldn’t be simpler: It just assigns default values to each property in the one and only DocumentTab object (dts[0]), blanks out TextBox1.Text and sets the document name (which is Untitled) as the text in the title bar (TitleText.Text).

NewCommand_Executed() was mostly OK. I just changed the one DocumentTab reference, dts[0].TextHasChanged, to dts[CurrentTabNumber].TextHasChanged. And I call a helper method in Tabs.cs called GetTabItemAndTabNumber() to make sure I have the correct index for the current tab.

But there is a call to a method I wrote called DisplayConfirmationDialog() in there too. And I spent a lot of time improving that method over the past few months after discovering a few bugs. I went down a side path in which I thought I might use a C# Tuple to solve the bigger of the problems I’d found. But in the end, I just made some edits to DisplayConfirmationDialog() that solved the issues.

Without getting to0 far into the weeds, the issues were tied to whether the current “document”–which might be nothing or some text in a TextBox–had been previously saved. If yes, then I would need to call my Save() helper method. If no, I would need to call my SaveAs() helper method. The code for .NETpad 3.0 has a vestigial third helper method, SaveOrSaveAs(), that I originally intended to call in front of Save() and SaveAs(), as it would determine which to call. But it doesn’t do anything, and so I updated Save() and SaveAs() instead and removed SaveOrSaveAs(). Now I use a Switch statement in DisplayConfirmationDialog() to make that test. And Save() and Save() both return a Boolean value now so I can determine whether the user saved the file successfully or not.

There’s more in there from a code cleanup perspective, but you get the idea. There are tests to see whether things need to be saved, and the user does or does not save the current document. Unless they cancel out of the operation, my NewDocument() method is called next.

This was pretty straightforward in .NETpad 3.0, as noted above. It just assigns default values to all the relevant DocumentTab object properties and changes TitleText.Text and TextBox1.Text.

The new version is more complex.

Now, we’re working with dts[CurrentTabNumber], and because the tab is not being closed, it’s the same DocumentTab object that was created whenever and associated with that TabItem. Its properties are changed to default values as before, but now the default for the DocumentName property includes the file extension (along with corresponding code changes throughout the project). And there is a new helper method called in there, WriteTabText(), that replaces the previously simple assignment to TitleText.Text. Instead, WriteTabText() handles the complexities of my dynamically created TabItems, specifically TabItem Header, which includes a TextBlock to display as much of the document name as possible, plus the Close tab button. Which can have two different states, an standard “X” or a bullet-like character to indicate that the document has unsaved changes.

That code is a bit ugly, but it’s what’s required. Long story short, the value in the DocumentName property for the current DocumentTab object is written to the TextBox, minus the file extension. And the correct Close tab button text (via its Content property) is displayed. If the underlying document is unsaved (TextHasChanged is True), it looks normal.

If TextHasChanged is False, it indicates that with this unique character (emulating Notepad).

And that, folks, is just one command. There were many code changes across all the commands and other methods in .NETpad to accommodate multiple tabs and documents. I’m still working through them, in fact, looking for more issues and places that could use some cleanup. In some ways, that never ends. But the multiple tabs and documents are complex enough I’m trying to be careful here.

And, to be fair, a few things didn’t need to change at all. In the constructor for MainWindow(), for example, which runs when the app starts up, there is a call to the LoadSettings() method in Backend.cs. Looking through that method, you can see that what that does is suggested by its name: It reads in the app settings–the app theme, app window size and location, font configuration, and so on–and applies them. This is a big method, but it didn’t require a single change because of the app’s multiple tab or document support.

That said, I did make one change to LoadSettings() to address a bug that had been, um, bugging me for a while: I only start the Timer if the user has configured the app to use Auto Save. Before, it turned it on regardless. And in the future, when I start working on session state again, there will be changes here because the saved state–which documents and tabs were open when the app closed the previous time–will be part of those saved app settings.

And so that’s good, too. As noted, fixing bugs that were already in there is a win too.

More soon…

There’s always more to do. To help me track the values of all the relevant tab-related data, I created a temporary panel that displays between the Menu and the main TextBox and updates on the fly. This has helped me fix some bugs, also a win, but it’s key to ensuring that each of the other commands–Open, Save, Save As, and so on–and other methods in the app all work properly as I update each and refine them as needed.

This is probably naive, but I feel pretty good about it. There will always been user experience issues related to tab overflow–what happens when there are too many tabs to display them all in the current size of the main app window–and there’s a lot more to do. But it’s getting there, and I can see the light at the end of the tunnel, so to speak. And when I get this work in a good place, I can move over to the next piece of this puzzle, which is implementing app session state related to the tabs, as Notepad does. When I wrote .NETpad 2025: What Comes Next (Premium) in early January, I thought I’d be doing that work before the tabs (which makes a bit of sense since I could have implemented it for a single document first). But I didn’t end up doing it that way, so that work will happen later.

But first things first.

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