.NETpad 2025: Tabs Next Steps (Premium)

The way that WPF implements tabs is old-fashioned, non-optimal, and in no way ideal for .NETpad. But I’m going to try to use it anyway. In this post, I will highlight the problems I face and discuss the progress I’ve made.

Basically, a brain dump. Apologies.

If you’re interested in this topic, and God help us all, it consumes me sometimes, then it’s worth reading .NETpad 2025: Tabs First Steps (Premium) if you haven’t. The short version is that I need to dynamically create tabs–TabItem controls in WPF-speak–using C# code. And this is complicated because the tabs I’m creating are what I think of as complex or compound tabs in which their constituent parts–the header that displays the document name and the content–can both be implemented using a grid of multiple controls.

In the previous article linked above, I described only the creation of the TabItem header in C#, which is successful enough. But there’s more to this. I have to programmatically access the header of any tab at runtime–such as when the user closes a tab, creates a new tab, opens a new document, and so on. And I have to figure out the content, by which I mean that I may not even need it. As this progresses, I’ll decide one way or the other.

Another complication: The layout used to display the Notepad main window–and thus for .NETpad as well–may dictate whether I can use TabItem content at all (or normally, as you’ll see in a moment). That is, because of the dated natural of the TabControl and TabItem controls in WPF, which, again, were designed for the world of 20 years ago, not 2025, and were never updated in a meaningful way. They’re better suited for static, old-fashioned user interfaces in which the TabControl is visually a container for two sets of controls, the header on the top and the content on the bottom.

But Notepad doesn’t use this design. In Notepad, the tabs and the content–the text box that takes up most of the space in the app window–are separated by a menu bar. So there are (at least) two choices here. I could …

  • Make the content a Grid that also contains the menu bar (i.e. it would contain the menu bar and the text box)
  • Use only the header–which is a Grid with a document title and a Close tab button–and ignore the TabItem content, and just lay out the menu bar and text box (and status bar and whatever else) outside the TabControl. I can maintain the document in each tab–i.e. what you see in TextBox1, the main app window’s text box)–using DocumentTab.

I implemented basic versions of both designs. But for all kinds of reasons, I have to use option number two. Reimplementing the menu bar for every single tab is stupid. But I also omitted an important bit: I also put a Find/Replace bar between the menu bar and text box, one that’s hidden by default. There’s just too much UI in there to do that, all inside a dynamically created TabItem. So I will ignore the TabItem content.

Except that I won’t. Maybe. I don’t know.

I added a Document property to DocumentTab so I can store the contents of the text file associated with the current DocumentTab object–and displayed in the main app window’s text box (TextBox1) when that tab is selected. For now, I also create the TabItem’s content as a TextBox, and I store the document contents there as well. This is clearly unnecessary and will change. But again, a work in progress.

That the visual design of Notepad butted into the limited capabilities of TabItem in WPF isn’t ideal. But it also served to focus my mind because it means some things simply aren’t possible. Working within this confine, and ignoring problems for Future Paul like managing the layout of the tabs and what happens when the user adds so many tabs that they “overflow” past the physical size/width of the window, I’ve spent a lot of time writing, testing, and rewriting my dynamic tab creation code. And to further what I described in .NETpad 2025: Tabs First Steps (Premium), the next steps I underwent are:

  • Handling tab selection, which includes displaying the Close tab window on the selected tab and hiding it on any other tabs
  • Handling tab close
  • Handling tab close on the final tab, which will close the app
  • Add a new tab
  • Handling the opening of a document so that it displays the title in the tab header and the document in the main app TextBox (and does all the other document state management I need)
  • Handling the switching of tabs so that the main app TextBox displays the correct document on the change

And … there’s more. But as always, baby steps.

Tabs.cs

Since writing .NETpad 2025: Tabs First Steps (Premium), I decided to separate all the tabs code into its own file, creatively titled Tabs.cs. This consists of several methods already–there will be more–starting, of course, with AddNewTab().

In that previous article, I only described the dynamic (C#-based) creation of each TabItem’s header, which is a Grid that contains a TextBlock and a Button. But I simplified that by getting rid of all the Grid layout stuff (columns).

For now, I’m also creating the TabItem’s content as a single item, which is ideal, a TextBox. I make it invisible, since the user will never see it.

Basically, I specify all the properties for the header’s TextBlock and Button, create a grid, and add those two controls to that grid. Then, I create a TextBox for the content. Then, I create a TabItem, assign the grid as its header, assign the TextBox as its content, and add that TabItems to MyTabs, which is the app’s TabControl.

So that’s AddNewTab() and it seems to work well. To test it, I’ve been running this method when the app starts up one to three times so I can get a feel for how well it looks. But this is also important for those other tasks noted in the bulleted list above. I also added a test button to the menu bar so I can view all the relevant data as I go.

Tab selection

If you observe how Notepad works, you’ll see that the current tab displays a Close tab buttons and the other tabs do not. By default, Notepad opens with a single tab, and because I’m not handling app-level state yet, .NETpad always opens with a single tab (for now), a single tab that displays an empty document. And so that Close tab button is visible. But when you open new tabs–via an Add new tab button that’s not there yet–the non-active tabs should not display a Close tab button. (Otherwise, you could close them without selecting the tab first, which, come to think of it, is how web browsers work. Moving on.)

In short, I have to handle the TabItem’s GotFocused() event. This will do three different things:

  • Hide the Close tab button in every available tab
  • Display the Close tab button in the selected tab
  • Display the document associated with this tab in the main app’s TextBox

The first two of those tasks require multiple lines of code, so I created two helper methods, HideAllCloseTabButtons() and DisplayTabCloseButton(), to handle them, respectively.

HideAllCloseTabButtons() iterates through each of the TabItems in MyTabs, finds the Grid in the Header, finds the second child element in that Grid, which is a Button (the Tab close button), and then changes its Visibility to Hidden. In short, it does what the name of the method suggests.

This code was not obvious to me. Indeed, programmatically accessing elements in a grid that is in a TabItem–basically, traversing a tree of elements embedded or contained within each other–was not at all easy to figure out. I Googled it, of course, and spent a lot of time on Stack Overflow. But I feel like this is uncommon in WPF for some reason. I even wrote a rather complex block of code that did achieve it ham-handedly.

But the way I work is that I rewrite and rewrite things like this to make sure that they really work. And on a second pass through this work, I had enabled GitHub Copilot in Visual Studio. And in a miraculous turn of events, I wrote the empty body of HideAllCloseTabButtons(), and it wrote that code you see above for me. Or something very close to it, I may have edited it slightly while testing it. But the net result is that it does what I want and with far less code. This is but a minor example of why GitHub Copilot is amazing.

With that out of the way, I needed to make the Close tab button of the current tab visible again. That happens in DisplayTabCloseButton(). This works similarly to the previous method, but I don’t need to iterate through all the tabs, I just need the current tab. So it gets the current TabItem, finds the second child element in that Grid, which is a Button (the Tab close button), and then changes its Visibility to Visible.

With those done, I can run these two methods back-to-back in the Tab_GotFocus() event handler. In other words, each time any tab is selected, the Close tab button in all tabs is hidden, and the Close tab button in the current tab is made visible (again). The net effect is that it works as it does in Notepad. Pretty sweet: As you can see here, the middle of three tabs is selected and only it displays a Close tab button.

Later, I added a third method in that event handler to display the correct document when the tab selection changes. But we’ll get to that later. Keeping just to basic tab management, the next task is to let the user close a tab.

Close a tab

Closing a tab involves clicking its Close tab button. And since that is a Button, I handle its Click() event handler. Of course, since the button–like the surrounding Grid, Header, and TabItem–is created dynamically in C#, I can’t just add that in XAML. I have to add it in C#. This was unfamiliar to me, but it’s easy enough to Google, and you can see how I assigned the Click event handler to tbCloseButton (the Tab close button) when I create an instance of that object.

But I also had to add the actual event handler method. In empty form, that looks like so:

Removing the current (selected) TabItem is simple enough, just a single line of code:

And this works, but it doesn’t account for the final tab. If you remove all the tabs, the TabControl becomes empty but the app window remains open. So I added a loop that checks for that. If it’s the final tab, the app window closes. Later, I’ll add checking to this to handle the event properly to prompt to save any unsaved documents and so on. For now, I’m mostly focused on doing the right thing with the tabs.

And it works. There’s more work to do around document state management, but it’s getting there.

Add a new tab

To make this easier to test, I added basic “Add new tab” functionality, with no checking to ensure the TabControl display doesn’t overflow over the other controls in the window. But adding the Add tab button was easy enough, since I could do it in XAML. I just added a button into the layout in TabGrid.

And the code-behind for its Click() event handler could not be easier: It just calls AddNewTab().

Adapt SettingsButton and BackButton

I previously replaced my previous custom title bar with a new version with a TabControl and Add tab button. So I had to add a few lines of code to SettingsButton_Click() and BackButton_Click() so that the top of the app looks right when the user displays settings and then returns to the main app window. This was straightforward enough. I just hide the TabControl and Add tab button when displaying settings:

And then redisplay them when the user backs out of settings. The net result is that it looks and works correctly.

Opening a document

With the basics out of the way–well, maybe just “roughly implemented”–it was time to look at the document handling functionality in the app and adapt it for tabs. As you may recall, late in last year’s project, I decided to bolt a basic version of the DocumentTab class onto .NETpad and change all the references to variables that assumed a single document to a list of DocumentTab objects called dts. Or, more accurately, the first item in that last, dts[0]. The idea being that this would make it easier to update the app later to support tabs and, thus, multiple documents.

This was the first meaningful test of that theory. So I started with the Open file operation. In .NETpad, this is handled by the OpenCommand_Executed() event, which does a quick test to see whether the current document has changed and then calls a helper method called OpenDocument().

As noted, It was hard-coded to dts[0], the DocumentTab object associated with the first tab. But this could be any tab. So that had to change.

Working the problem, I started to make a list of the things .NETpad would need to do repeatedly, like find out which tab was current. And what information was tied to that, and where/how I might reference it all. In other words, state. This evolved as I went, of course, and I’ve lost track of the order in which these things transpired. But whatever. There are tabs, which is a UI issue, and what I think of as an app state issue. There is the data associated with each tab, which is document state. And then there has to be some way to link them up and make sure they never lose track of each other. That is, if there is a list (or collection, or whatever set) of tabs, each member of that list should have an associated DocumentTab in dts. And vice versa. dts[13] should always map to TabItem13.

There is a right way to do this, I think. And that means I need to figure out WPF data binding at the very least. And probably the related MVVM (Model-View-ViewModel) app design architecture (or whatever modern alternative), a way to logically separate the user interface (the View), the data (the Model), and the code that binds the two together (the ViewModel). There are complexities to this type of thing but there is also elegance. I will continue trying to figure that out, but it’s fair to say I’ve taken baby steps towards some kind of design pattern by modularizing my code and, more explicitly, by creating the DocumentTab class for managing document data.

But for now, I just wanted to see if I could ham-handedly keep the tabs and the documents they represent synced up in basic ways. At the top of the MainWindow class declaration, I was already creating that list of DocumentTab objects called dts like so:

public List dts = [];

And because the tabs and the documents they “contained” are part of the main app UI, I figured it would be helpful to have a window/class-level variable for tracking the current tab (and thus dts) number (or index). I’m still not sure if this is necessary, but I also made one for the current TabItem. Like so.

public static int TabNumber = 0; public TabItem CurrentTab = new();

When the application starts, I call AddNewTab(). As noted, this creates a TabItem in C# code, but in doing so, it also adds that TabItem to MyTabs (the TabControl in the main app window) at index TabNumber and creates a new DocumentTab, adding that to dts at index TabNumber. I also made a few changes to the DocumentTab class to address the linkage between the two. There’s a new TabItem property called TabName, and a new DocumentTab constructor that takes a TabItem as an argument and then assigns that to TabName.

So that’s what I use when the app starts. I create the new TabItem, add it to MyTabs, and create a new DocumentTab by passing that TabItem to the constructor. The last thing that AddNewTab() does is increment TabNumber so that the next tab is given a unique name (the TabItem name is “TabItem” + TabNumber.ToString()) and a unique index that is identical in both dts and MyTabs.

So.

I routinely need to get the “number” (the index) of the currently selected tab (TabNumber). I also may need to get the current TabItem (CurrentTab), though as I work through this, I may find that’s not as necessary. But the trick is actually doing those things.

This may seem inelegant. But because each TabItem is named by concatenating the string “TabItem” with a number, I can simply use a C# method to extract the part of the name that is a number to get the index of the current tab. Then, I can assign it to TabNumber. It’s just one line of code, too:

TabNumber = Int32.Parse(string.Concat(((TabItem)MyTabs.SelectedItem).Name.ToString().Where(Char.IsDigit)));

OK, it’s a long and ugly line of code, but that’s fine. I hid it in a help method, as noted below.

Getting the current TabItem, i.e. CurrentTab is much simpler: All I have to do is get the index of the current item in MyTabs and cast it to a TabItem. Like so:

CurrentTab = (TabItem)MyTabs.Items[TabNumber];

I will need to do this so often that I created a helper method called GetTabItemAndTabNumber() so I can update TabNumber and CurrentTab at any time. Obviously, this is a candidate for data binding: It’d be nice if this just happened automatically. But this is what I’m doing for now.

OK, back to OpenDocument(). The first change was to add a call to GetTabItemAndTabNumber() at the top of the method and get those two variables up-to-date. The code that runs when a document is opened from disk still works as before by putting the contents of that file into TextBox1.Text. But I also need to update the proper DocumentTab item (dts[TabNumber]), update the corresponding TabItem content (the hidden TextBox, which, again, may be unnecessary and removed later), and display a truncated version of the document’s file name in the tab’s header.

And that looks like so:

The dts[] stuff is pretty obvious. But here’s what’s happening with the other code in there.

((TextBox)((TabItem)MyTabs.SelectedItem).Content).Text = TextBox1.Text;

That single line of code puts the contents of the opened document, which was just copied into TextBox1.Text, into the (hidden) TextBox that is the content of the current TabItem. I originally wrote this as multiple lines of code that created TabItem and TextBox instances and then put it all together, but I manually condensed it into what you see there, in one line.

WriteTabText();

I tried, but failed, to do similarly for the code that’s needed to change the current TabItem’s header text. So I just created a helper method called WriteTabText() and do it there instead. That looks like so:

And with that done, I could test opening documents in different tabs and make sure they worked correctly. Well, almost. I also had to handle tab switching, as noted above in the Tab selection section. The key there from the perspective of opening a document is that when you switch to a tab, TextBox1.Text–the main app text box–changes to display whatever content is associated with that document. That is, it displays the correct document.

Originally, I did that with this line of code:

TextBox1.Text = ((TextBox)((TabItem)MyTabs.SelectedItem).Content).Text;

But as I work through this, I can see that the TabItem’s content (the hidden text box) is probably unnecessary. And so it occurred to me that this method could simply call GetTabItemAndTabNumber() and then change TextBox1.Text like so:

TextBox1.Text = dts[TabNumber].Document;

This works identically, and is cleaner. And, yeah, I’ll be getting rid of the TabItem content stuff as I work to make this whole thing better.

Anyway, by this point, I could open whatever number of tabs–ignoring the overflow issues that occur when I open too many–switch between each tab, and then open whatever document in each tab.

It seems to work fine. It needs to be cleaned up, a lot. But it’s roughed in pretty well. I think.

Next …

I’ve started working on the document save methods in a similar fashion. I don’t want to get too far into this before I clean this up dramatically, either with data binding or data binding and MVVM (or whatever). So I will continue that work as well. But I feel pretty good about what I’ve done here. As noted, tabs are complex, and while just getting to this point was daunting, there’s still a lot more to do. As always.

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