OK, I’m getting ahead of myself here. But it’s impossible not to look past 2024’s .NETpad modernization work. There’s more work to do. The biggest being the long-awaited–but also long-dreaded–tabbed user interface.
Tabs. It was always going to be tabs.
Microsoft in 2023 added a tabbed user interface to Notepad in Windows 11, along with related features so users can choose where to open new documents (in a new tab, or in a new window) and “session” support by which Notepad will, by default, reopen with whatever documents and tabs you were previously using. This was a fairly incredible set of changes for a seemingly simple app that had handled a single document per window for its entire previous existence over 20 years or so. And given how challenging it is to implement this interface in .NETpad, it’s both amusing and alarming to me that these changes to Notepad were met with a collective shrug, with many wondering what took so long.
I think I may have some insight into that. Retrofitting a tabbed user interface onto an existing app is a major undertaking. Aside from the obvious user interface changes, it requires a thorough rethinking of the app’s architecture. Previously, document management was relatively easy. There was one document per window–meaning, one document per app instance–and I only had a few things to track in that regard. Key among them:
Even this simple document state could be difficult to handle accurately–as I always feel obligated to point out, I’m not a professional developer–and I of course made my life even more difficult for myself by introducing a related state that Notepad still doesn’t handle to this day: Auto save. If the user enables Auto save, there are more things to keep track of, and this feature was a source of constant logic issues for me throughout this past year’s modernization effort.
And yet, I can’t stop thinking about making my life even more difficult still by implementing the tabbed user interface that Notepad offers. I’ve described the issues I’ve run into with theming and styles previously, but leaving that aside, I’ve spent a lot of time thinking about and, more recently, trying to implement basic support for tabs–and, thus, for multiple documents–in .NETpad to prepare for this future change. And while the theming and style issues remain, and continue preventing me from adding this feature, I think I’ve done enough work to at least discuss how I think I may handle it. (I also have thoughts on working around the theming issue, but one step forward at a time.)
When the current WPF version of .NETpad fires up, it creates several “global” variables, or application state variables, that I use during runtime to track various things. Using the examples noted above, the three relevant examples are:
In a future version of .NETpad that supports tabs, this will be more complicated. There won’t be one app state variable for each of these things, there will be one of each of these things for every open tab. And because there’s no way to know how many tabs a user may open, I have to think of a new way to access (read and write) these values.
C# supports arrays, lists, collections, and similar language constructs so one can manage groups of related objects. And Microsoft provides WPF with a TabControl control that can contain one or more TabItem controls. You can reference those TabItems by name, as I’ve done with various standalone controls. But in keeping with the dynamic nature of this interface–where the user can arbitrarily add more tabs or remove tabs at any time–it makes more sense to skip the names and reference them as items in a collection. By index.
In other words, I might have previously used simple code like the following to change the value of TextHasChanged for the one and only document the old app (instance) might provide:
App.TextHasChanged = false;
But now I need to reference the TabControl (which might have a name like MyTabControl), identify the currently displayed TabItem (which will remain unnamed) by its index, and then access some set of variables that will be available in each TabItem (“in each tab” or “in each document, each of which is in its own tab”). And … that needs some thinking. Or needed some thinking. I feel like I’ve landed on a viable approach.
The TabControl has a name. And it has an Items property that represents the 0-based collection of TabItems it contains. So the first (default) tab is MyTabControl.Items[0], the second (if present) is MyTabControl.Items[1], and so on. TabItem objects have their own properties, like Header, which represents the “text” or “caption” of that control, i.e. what you see the tab itself. (Actually, it can be a lot more complex than just a single string of text, but let’s keep it simple here.)
The Header of each TabItem will map directly to the DocumentName associated with the relevant document, so that one is simple: Instead of displaying the document name in the title bar of the app, we will display it in the relevant tab. But we need to associate other things with that TabItem. Including the document itself, which I’ll get to. But first those basic state variables noted above.
To test this idea, I created a simple C# class to handle this set of per-document data. And while it will get more complex in time, for the high-level purposes of this conversation, it looks like so for now.

I call the class DocumentTab because its contents represent a document and the tab that will contain it. It’s defined in App.xaml.cs because this is an application-level construct (to my mind), but I suppose you could make a case for moving it elsewhere. Please don’t. 🙂
It has three properties–TextHasChanged, DocumentIsSaved, and DocumentName–that map to the application state variables I used previously.
In C#, you call a constructor when you create an instance of a class. And you can have multiple constructors, each of which can provide a different set of arguments. For now, I have two constructors, though I’m only using one of them for now. The first (public DocumentTab()) has no arguments: It applies default values to each of those properties at creation time. So TextHasChanged and DocumentIsSaved are both false by default, and DocumentName is set to “Untitled.txt”. But the reason there’s a second constructor (DocumentTab(bool text, bool doc, string name)), one in which you supply the values for each at runtime, is that we’ll need to use this class for existing documents, and not just new documents. I’m already imagining a few other constructor types, but, again, I’m keeping it simple for now.
With the basic class outlined, I created a basic version of .NETpad with a (new) TabControl (called MyTabControl), a basic menu, a basic textbox, and a basic status bar. MyTabControl starts life with a single tab, and I’m playing around with a more complicated layout for the header, but for now, don’t worry about that. It’s pretty basic.

There are three menu items for testing things: “Toggle Saved” and “Remove Tab” over on the left, and the gear icon on the right that normally opens Settings, though I’ve repurposed it for now for “Add Tab.”
“Toggled Saved” should toggle the value of DocumentIsSaved for the currently selected tab between true and false when clicked.
“Remove Tab” should remove the currently selected tab when clicked. (With just some basic error-checking for now.)
And “Add tab” adds a new tab when clicked. Each tab it adds gets a number–its index–that maps to its position in the TabItems collection in MyTabControl. So the first one you add (which is the second tab) is “1.” The next one is “2,” and so on. (The tab the app starts with is index “0.”)
When the app runs, I do the following:

To understand the new and different way of referencing these values, consider the following.
When I change the title bar in the current version of .NETpad, I might use code like this:
AppWindow.Title = App.DocumentName;
But to change the header of the currently selected tab similarly I will need to use code like this:
CurrentTab = (TabItem)MyTabControl.Items[MyTabControl.SelectedIndex + 1]; CurrentTab.Header = dts[0].DocumentName;
This is a lot harder to read. And conceptualize. But once I got over that hump, I started adding some basic features. Here are the first three.
Adding a new tab is pretty straightforward, especially when you do it indiscriminately with no error checking. I have to:
That code looks like so.

And it works fine, assuming you don’t care about tab overflow and whatnot. It’s early days.

To toggle the saved state of the document (that doesn’t exist yet) that will be displayed by the currently selected tab, I need to:
That code looks like so.

This works fine as well.

This one will require even more work around error checking, and I need to figure out a system for selecting a different tab when one is deleted. But for now, it works within reason. The code has to:
It looks like this, for now.

And this seems to work OK as long as I don’t screw around with it. The big thing here, and with the other event handlers, is that each action occurs against the correct item(s), meaning the correct (visible) tab (TabItem) and the correct (under the covers) DocumentTab object that represents its contents (or will, as this improves). For example, if there are four tabs (MyTabControl.Items[0], [1], [2], and [3]) and I delete the third one (MyTabControl.Items[2]), the dts list of DocumentTab objects should change to reflect the removal (so that there are only three items now, [0], [1], and [2], the latter of which used to be [3]). And the tabs should visually shift to accommodate the removal. And … it does appear to work. (Removing a tab doesn’t trigger a redrawing of the other tab headers, but it should.)

Granted, these are just the first baby steps into the underpinnings of this new tabbed-based architecture. It will only get more complex as more of the old app’s features are added, most importantly those tied to the actual document each tab will display. But this is the basis, I think, for that coming work.
Next steps?
I think the next logical thing to do is to adapt the current app to use this system with just a single tab. Basically, move the app-based state logic over to be tab-based. And then once it works normally like that, I can start working on add, removing, and otherwise managing an arbitrary number of additional tabs. There is a lot of other work in there related to tab display and management, for example what happens when there are too many tabs to display on a single row or within the width of the window. But this is the start, I think.
And I still need to “finish” the work I started this past year, the (non-tab) modernization of the current app. I will keep working on that as well, of course.
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.