WinUIpad: Word Count, Text Cursor Position, App Shutdown ⭐

WinUIpad: Word Count, Text Cursor Position, App Shutdown

I was hoping to keep this to weekly updates, but two weekends came and went with me struggling to figure out why WinUIpad was crashing on app exit in certain conditions. So another few weeks with the Windows App SDK and WinUI 3 was another few weeks of frustration, as always. But that’s the job, and this new update includes basic implementations of the Word count and Line and column position displays, plus one of the dumbest and most difficult tasks to do properly with this framework, app shutdown.

Here’s what’s new.

Word count

Looking over my WinUIpad to-do list, I decided to tackle a few of what I felt would be the easier tasks. But as is so often the case with the Windows App SDK, I had mixed results.

The simplest, perhaps, is the Word count display in the status bar: As the user types, I just need to update the count. This is a feature I added to the first, Windows Forms-based versions of .NETpad dating back to perhaps 2019. And it’s been in each version ever since.

For this version of what’s now called WinUIpad, I decided to look at the code I had written or, in this case, obtained previously. And I found a March 2020 update to .NETpad that uses a regular expression to output the word count. Which is how I know I didn’t write it.

You’ve heard the phrase that the best phone is the one you have with you. In a similar vein, the best code is often the code you don’t need to write. And though I’m sure I will revisit this later as part of an app-wide refactoring, the goal for now it not get stuck on details and to just get it done. So I went back to the well.

As with previous versions of the app, a lot of the action takes place in the TextChanging() event handler for the main TextBox. Three things now happen when this event handler fires: It changes the Document object instance’s TextChanging property to True, changes the Document object instance’s Contents property to the TextBox’s Text contents, and, now, it also runs a new helper method called UpdateCount().

As for the new UpdateCount() method, this is based on the aforementioned 5+ year-old code.

That TextBox has two event handlers (the other is SelectionChanged() and is discussed below), but UpdateCount() only needs to be called from here. So that’s that. For now.

Line and column position

This I haven’t completely solved, though I’ve left the code I have so far in there for now. The issue is that the Windows App SDK/WinUI 3 TextBox control doesn’t support WPF’s GetLineIndexFromCharacterIndex(), CaretIndex(), and GetCharacterIndexFromLineIndex() methods, and so you have to do all that work yourself. I know, crazy.

Since WPF is open source, it’s possible to look at the source code for those three methods, but for now I am trying to just figure it out using GitHub Copilot, which is an endless circle jerk of errors when it comes to Windows App SDK/WinUI 3, and my own limited capabilities.

Anyway, as per the UpdateCount() method noted above, I create an UpdatePosition() method for the line and column position display in the status bar. I still have questions about which TextBox event handlers need to call it, but for now it’s called only from SelectionChanged(). I’ve been experimenting with updating this display in response to mouse-based events, since you can change the text caret cursor position that way too. But the Windows App SDK is unsufferable.

Anyway, this doesn’t work. Meaning, it sort of works and then it doesn’t as you move the text caret cursor around. And there are other issues, most obviously when Word Wrap is enabled. But I’m over worrying about that: I will make this work with non-wrapped text and that will likely be the extent of it.

App close

This is a huge problem with the Windows App SDK and it’s something I’ve struggled with again and again and again. Put simply, if the user closes the app, no matter how they choose to do so, I need to test to see if the current document needs to be saved and, if so, prompt the user (if it’s a new document) or just save it (for an existing document) before actually closing the app.

This is straightforward in WPF. It is not at all straightforward in the Windows App SDK/WinUI 3. Worse, GitHub Copilot is literally unable to figure this out, and it gave me so many wrong answers that I finally ran into the monthly GitHub Copilot usage limit for free help. Which sucks because it was GitHub Copilot that used up my free credits with wrong answers. (Hence the circle jerk comment above.)

The issue is that there are multiple ways the user can close the app (the File > Exit menu, typing Alt + F4, clicking the Close window button) and multiple event handlers one can use to run code that occurs before or as the app/window is closing. And none of these event handlers can call the others.

The two most obvious event handlers are Window_Closed(), for the window, and ExitMenu_Click(). But there’s also an OnClosing() event handler for the AppWindow object that you can add manually, and that showed some promise and ultimately worked better for my needs than Window_Closed().

When GitHub Copilot made so many mistakes that it used up my monthly free credits, I was luckily just a day away from the month resetting. So a few days later, I went back to GitHub Copilot and wrote of its previous answer:

“None of that works properly. In the OnClosing method, I check to make sure the document needs to be saved. The code you added to ExitMenu_Click doesn’t do that.”

It then took the code I had written for OnClosing() and copied it into ExitMenu_Click(), the type of code duplication I am trying to avoid. But as I’ve learned with the Windows App SDK/WinUI 3, the dumbest answer if often the right answer. So I will leave that as-is for now and think about just creating a helper method with most of that code so that it’s a little more sophisticated.

For now, as noted, the code is basically the same in both OnClosing() and ExitMenu_Click(). The big difference is that OnClosing() needs to defer the event that’s occurring, meaning the window (and thus the app) is trying to close. And that, at least, works similarly to WPF. The OnClosing() method has two parameters, sender and e, the latter of which is of type AppWindowClosingEventArgs. And that object instance has a Cancel property you can set to defer the window (app) from closing.

It basically looks like this.

private async void OnClosing(object sender, AppWindowClosingEventArgs e)
{
    // Prevent the app window from closing immediately
    e.Cancel = true;

    // Code for handling whether the document needs to be saved
    // Code for saving the app settings

    // Now you can close the window/app
    e.Cancel = false;
    Application.Current.Exit();
}

And ExitMenu_Click() is basically the same, but without the e.Cancel stuff. So it “works,” I guess. It’s just not that elegant.

As for the app crash error that was bedeviling me for the past two-plus weeks, that was partially my fault. Because the Windows App SDK and/or GitHub Copilot required me to basically duplicate the code in the App object’s OnClosing() method and in the MainWindow’s ExitMenu_Click() method, I created a BeforeClosing() helper method for both to call to help reduce code reuse a bit.

This proliferation of helper methods is a bit of a problem, but this one does a few checks and calls the FileOperation object’s NeedsToBeSavedAsync() method (which in turns will save an unsaved document if the user chooses to do so) and then saves the app’s settings before exiting. Or not exiting, since the user could also cancel at any time.

In any event (ahem), my Save As functionality (in SaveAsDocument(), part of the FileOperation class) is called twice, once from the FileOperations class (which it is also part of) and once from MainWindow. In the Windows App SDK, windows like the Save As dialog need an old-school (Win32-style) handle to the parent window (MainWindow in this case). That’s easy enough from MainWindow, but the way I was doing it from FileOperations was incorrect and causing the crash. Anyway, it’s fixed now.

And it needs to be cleaned up. Before I figured out what was wrong here, I added an App-level property called AppCanBeClosed that’s now being checked in several places too. I think I can remove that and will look at that 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