
Flush with my success in rewriting .NETpad 4.0 from scratch and reimplementing features in reverse, I moved on to file operations–New, Open, Save, Save As, and Close–next.
The goal? To implement file operations as a standalone C# class that’s separate from the MainWindow class. I’ve referenced this need throughout the app a few times, but only in passing. But as .NETpad has grown more sophisticated, my ham-handed design, in which the core app feature have been modularized into separate files (Backend.cs, AppSettings.cs, FileOperations.cs, and so on) but most are just part of the MainWindow class that was originally defined in MainWindow.xaml and MainWindow.xaml.cs, has become problematic. This, AI has complained, is too many lines of code for a single class.
Fair enough. Creating standalone C# classes for specific functions like file operations is architecturally “better.” But it also brings some complexities along for the ride. When everything is in the same class, visibility into and access of other parts of the app are simple, as there are no permission boundaries to worry about. That is, a method or variable or whatever in, say, Backend.cs can reference anything in any other file that’s part of the same MainWindow class. Which is/was a lot of the app. (Assuming the visibility of those things is public, etc.)
But standalone classes can be challenging (to me) and I’ve had some successes and some defeats. Before the rewrite, for example, I successfully created an AppState class to handle what I think of as app-level variables. These were global to the app (via App.xaml.cs) or the MainWindow (via MainWindow.xaml.cs), and they handled things like the zoom level, whether spell checking is enabled, and so on. Less successfully, I looked at moving the tabs-related functionality in Tabs.cs into its own class or into DocumentTab.cs. And I couldn’t get to a point where doing either made any sense.
But file operations, thankfully, has landed firmly in the win column. As I write this, I’ve successfully moved the file open, save, and save as functions into the new class. The new operation doesn’t need to be part of the class, and described below. And I will get close to work, for sure, but it requires some extra attention as described below and will come next.
So let’s turn file operations into a class.
The new version of the app still has a FileOperation.cs file, but this time it defines a standalone class called FileOperations. It started out like so.

I don’t believe this class needs a constructor: I will just create an instance of the class when needed, run whatever method–for open, save, save as, and so on–and that’s that. File operations are temporary.
Naturally, I would start with New.
For this app, the New operation could be considered “new document,” “new tab,” and/or new DocumentTab. In Windows Presentation Foundation (WPF), New is one of many built-in commands in the WPF commanding system, and so I implemented it as a command in the previous versions of the app. And then I adapted it to mean “New tab” in the previous (public, 3.0) version.
There’s a lot of WPF-specific language to commands. As Microsoft explains:
In .NETpad 3.0, the New command is defined in a <Window.CommandBindings> section in MainWindow.xaml like so:
<CommandBinding Command=”New” Executed=”NewCommand_Executed” />
There’s also a shortcut key binding in the <Window.InputBindings> section in the same file. This used to be bound to Ctrl + N, of course. But now that .NETpad supports tabs, I bound it to Ctrl + T instead.
<KeyBinding Gesture=”Ctrl + T” Command=”New” />
Then, within the code for the File menu, I added the app’s first sub-menu item, for New, which now displays as “New tab.”
<MenuItem Command=”New” Name=”NewMenu” Header=”_New tab” InputGestureText=”Ctrl+N”>
This binds this menu item to the New command. (And adds back the Ctrl + N keyboard shortcut, so it works the same as Ctrl + T now.)
The NewCommand_Executed() event handler checks to see whether the document displayed with the current tab needs to be saved. And then it calls a helper method I created called NewDocument(). Which in the previous (pre-write) version of this app is in FileOperations.cs, of course. But looking at this code, I found something interesting. NewDocument() used to have a lot of code in there, but as I evolved the tabs-based version of the app, I realized that I could simply call AddNewTab(), which is still in Tabs.cs and is still part of the MainWindow class. (I had previously edited AddNewTab() so that doing this made sense.)
Well, AddNewTab() has a lot of code in it. And while I could put NewDocument() into my new FileOperations class, why bother? All it does is call a single method. Changing that method to work across classes–with a return value and one or more parameters–was more work than necessary. So I left NewDocument() out of the FileOperations class, and out of the code entirely. Now, NewCommandBinding_Executed() just calls AddNewTab() directly. Why not?
That’s rhetorical. There may be good reasons to add this later. But for now, I punted.
This is where things get interesting, since the Open command would obviously need to call some class method. In .NETpad, the Open command is defined in <Window.CommandBindings> section, like New is. But there’s no need for an Input binding. So I added the XAML code for the Open sub-menu item under “New tab” in the File menu. Plus the OpenCommandBinding_Executed() event handler in MainWindow.xaml.cs it requires.
In the pre-rewrite version of this app, OpenCommandBinding_Executed() checks whether the document displayed needs to be saved and then displays a confirmation dialog if so. If the user saves the document or declines to save it, it runs the OpenDocument() helper method in FileOperations.cs. (If they cancel, the Open command is canceled.)
I don’t have a confirmation dialog yet, that will come later. So my new version of OpenCommandBinding_Executed() temporarily skips the check for an unsaved document. But I would need an OpenDocument() method in my new FileOperations class, one that can be based on the code in the non-class version of OpenDocument(). So I got that started, but then I had to check to see what objects the old OpenDocument() methods accesses in the MainWindow class. There were two:
You can see some of that here.

I can’t directly access those MainWindow objects from FileOperations. So I would need to pass references for one or both of them to the new OpenDocument(). Or none of them, as it turns out. Instead, I pass in the current DocumentTab object, which has a Document property representing the document that will fill TextBox1.Text. And I pass it the current tab by its index number to line things up.
Maybe the code will make sense of this. OpenCommandBinding_Executed() now looks like so:

And then the OpenDocument() class method I’m calling is structured to accept that DocumentTab object:
public bool OpenDocument(DocumentTab dt)
Again, I’ll add a check for an unsaved document once I figure out a new confirmation dialog. For now, the event handler for the Open command gets the index of the current tab, creates a new FileOperations object, and then calls its new OpenDocument method as part of an if statement. If the user goes through with the Open operation, and it succeeds, this method returns true, and then the UpdateTab() method in Tabs.cs runs. The key here is that I pass the OpenDocument method a reference to a DocumentTab object: This method will update that object if everything goes well, and then UpdateTab() will literally update the tab using the new data that DocumentTab contains.
To adapt OpenDocument to the class, it works much like before. But it no longer updates TextBox1.Text directly; that happens afterward in UpdateTab(). OpenDocument() now looks like this.

Note that I am skipping the custom Message box display when there’s an error for now (that will come later too). And there’s a new ResetToDefaults() method I created in the DocumentTab class that does what it says it does. (This is simpler than just creating a new DocumentTab object as I want to keep the tabindex (and thus the TabItem name) and the DocumentTab lined up.)
And then UpdateTab() looks like so.

This updates both the tab header (via the WriteTabText() method I discussed previously) and then TextBox1.Text, which used to occur inside of OpenDocument.
This seems to work great. I can now open documents normally, and as before. But without some unsaved document and error checking. I can switch between open tabs and the information (document name, contents, etc.) is retained. It all works.

Save and Save as have been a constant source of internal debate and each time I reexamine this part of the app, I think through it again. That is, Save and Save as are related and similar, but they’re also different.
The Save operation is needed for the following:
But the Save as operation is more nuanced. It will always display a Save as dialog, and it’s needed for:
And so the debate here is whether Save and Save as are one operation or two operations. In WPF, there’s no debate, as it has two separate commands and they can be triggered by UI or by their own keyboard shortcuts. But in my app, I have Save(), SaveAs(), and SaveOrSaveAs() helper methods. And I once again examined these things to see whether they could be simplified in whatever ways, now that they are moving into a separate C# class.
As before, I added the necessary Command bindings, Input bindings, and menu items to the XAML for MainWindow. And I added the blank SaveCommand_Executed() and SaveAsCommand_Executed() event handlers to its C# code-behind file. In the pre-rewrite version of this app’s code, each of these calls a single helper method. SaveCommand_Executed() calls SaveOrSaveAs() and SaveAsCommand_Executed() calls SaveAs(). In this version of the app, those helper methods would be in a class, so I would need to adapt them as I did with OpenDocument(). In the end, I settled on the following for the event handlers.

Those are almost identical so, yes, I’m considering converting this into a single event handler. But for now, each does the following:
SaveOrSaveAs() checks whether the current document is already saved. If it is it, it calls Save(), another helper method in the FileOperations class. If it isn’t, it calls SaveAs(). In both cases, it passes it the tab index and TextBox1.Text.

Save() doesn’t need to display a dialog. It just saves the existing document and then updates the associated DocumentTab object’s TextHasChanged and DocumentIsSaved properties to the correct values.

SaveAs is a little more complicated. It has to create an instance of the (system) Save as dialog, display it, and then accept whatever choice the user makes. If they elect to save the document, I update the relevant DocumentTab object’s DocumentName, TextHasChanged, DocumentIsSaved, and Document properties and save the file. (I thought I’d call ResetToDefaults() here, but I didn’t need to.)
And this works great, too. Save and Save as seem to work just fine, whether it’s a new or previously saved document.

This was going well. I started to believe that nothing could prevent me from finishing up the file operations.
The Close command prevented me from finishing up the file operations. There are all kinds of problems here, it’s just a complex part of the app. And on two levels.
First, “close” is now three different things: Close tab, Close window, and Exit. Should those last two be the same thing? I already wrote Close tab as part of the initial rewrite work, but should the Close command be associated with Close tab, as it is now, or Close (or Exit)? What is close??
In the end, I kept Close–the command–as-is. That is, it’s tied to Close tab. But the second issue with Close is that this really needs to be tied to some checks. And it’s the same set of checks I’ve largely ignored during the rewrite. That is, you can’t just close a tab or the entire app. You need to make sure that nothing has to be saved first.
On a per-tab based, there are two checks:
On an app basis, there are likewise two checks:
This set of checks and confirmations is one of the big issues I was wrestling with before the rewrite, and it is the key reason I decided to rewrite the app in the first place. I want to get this right, to perform the checks efficiently and in the right place(s), and I was hoping I’d magically stumble onto the right approach given the success I had had so far with the rewrite. But it remains a tough issue. So I will keep working on it. And will leave that work, and whatever solution I come up with, for next time.
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.