The UWP Files: Unsyncable (Premium)

I had almost given up on the Universal Windows Platform (UWP) version of .NETpad multiple times because of ongoing difficulties with asynchronous operations. And then the answer came to me in a dream.

No, I’m not kidding.

Like many of you, I suspect, I’ve been sleeping poorly. And last night was particularly bad for no reason I can explain. But it was clear that I was going to need to take a nap today, and with Andrew canceling the already rescheduled recording of What the Tech, I suddenly had an extra two hours this afternoon. So I figured I’d work on the UWP version of .NETpad until I inevitably started drifting off.

I have restarted this project four times since the first version. Each iteration of the project doesn’t correspond exactly with the articles I’ve written about my experiences so far. But if you go back and look, you can see that I started with a version that used a traditional menu-based UI, similar to the original Notepad, which is, of course, a desktop application, and then evolved it to utilize a more modern interface that was better representative of UWP.

That took a lot of time, but I created a Settings pane that slides in from the right and custom Content Panel dialogs for the Save prompt and Find/Replace. Along the way, I also started building out the app’s other functionality, including the important file operations like Open, Save, and Save As.

And virtually all of these things triggered huge problems, all of them related to UWP’s requirement that any pop-up windows and all file operations occur asynchronously. I first wrote about this issue back in The UWP Files: Asyncing Feeling (Premium) last week, but this has been an issue for much longer than that.

I’ve researched this topic more than I care to admit, and I even reached out on Twitter recently in the vague hope that someone would understand the issue and provide some insight. But I was starting to give up, was starting to reconcile myself with the notion that this version of the app—which because of other limitations was already going to be somewhat of a subset of the WinForms and WPF versions of the app—would never be finished.

But I kept plugging away. In part because there was just too much code to wade through in the versions in which I had implemented the reading and savings of user settings, the display and function of the Settings pane, and much of the implementation of Find, Replace, and Replace All, plus a ton of smaller features, I decided to focus only on the problem areas for my fourth restart of this project. I built the basic structure of the app, implemented the simplest possible Save prompt (as a Content Dialog), and dug into file operations.

This version of the app has only three command bar buttons, for now: New, Open, and Save. It uses the two key global variables used to track changes in the app, TextHasChanged and DocumentName. And it has three key custom methods, DisplaySavePrompt, Save, and SaveAs.

TextHasChanged is a bool (Boolean) that starts off as False, but changes to True whenever the user makes a change to the text in the textbox. Simple:

if (TextHasChanged == false)
    TextHasChanged = true;

But I ran into an issue here where I’d open a file and the value of TextHasChanged remained True. So if I immediately clicked New or Open after that, the Save prompt would appear even though nothing needed to be saved. To work around this, I researched whether there was a way to determine whether the change had come from some direct interaction with the textbox. And so that code evolved into this:

if (TextHasChanged == false)
    if (TextBox1.FocusState == FocusState.Keyboard | TextBox1.FocusState == FocusState.Pointer)
        TextHasChanged = true;

That seems to do the trick, though I’m now wondering if my more recent dream-based epiphany about asynchronous code will render the workaround unnecessary. But I’ll get to that.

DocumentName starts off as an empty string:

public string DocumentName = "";

But if I open an existing file or save the document, DocumentName is assigned the name of that file. When .NETpad is displaying a file of some kind, the length of DocumentName is greater than 0. When it’s not, the length is 0.

TextHasChanged and DocumentName together play a key role in determining what, if anything, needs to be done first when the user clicks New, Open, or Save. That is, there may be something that is unsaved, and the user needs to be prompted if so. Because of that, I use a single custom method called DisplaySavePrompt, that handles this. It returns a bool; if the user selects Save or Save As, it returns true. If they select Cancel, however, it returns False, indicating that we need to short circuit the operation (pressing the New, Open, or Save button) that triggered the Save prompt too.

DisplaySavePrompt takes the following form:

if (TextHasChanged == true)
{
    if (DocumentName.Length == 0)
    {
        // Display the Save prompt, run Save() method if the user chooses Save
        // return true if they choose Save or Save As, return false if they choose Cancel
    }
    else
    {
        // Display the Save prompt, run SaveAs() method if the user chooses Save
        // return true if they choose Save or Save As, return false if they choose Cancel
    }
}
return true;

If you look at the Click event handler for the New or Open button, you’ll see the first lines of code are always the same:

bool result = await DisplaySavePrompt();
if (result == true)
{
    // do whatever you need to do here
}

In other words, if the user chose Cancel, nothing happens.

I don’t do this check in the Save button’s Click handler because it’s not necessary: If the user manually clicks the Save button, I just save the existing file, if that makes sense, or display a Save As dialog. But that’s handled with two custom methods, Save() and SaveAs(). So the Save button’s Click handler looks like so:

if (TextHasChanged == true)
{
    if (DocumentName.Length == 0)
        SaveAs();
    else
        Save();
}

What I’ve not described here, of course, are the asynchronous operations.

The simplest is for the Save prompt, which I now implement as simply as possible in XAML as a Content Dialog like so:

<ContentDialog x:Name="SavePrompt"
                      Title="Save changes"
                      Content="Do you want to save changes before closing?"
                      PrimaryButtonText="Save"
                      SecondaryButtonText="Don't Save"
                      CloseButtonText="Cancel">
</ContentDialog>

We display this Content Dialog via the DisplaySavePrompt method, which I originally structured like so:

private bool DisplaySavePrompt()
{
    // code here
}

But since the display of sub-windows happens asynchronously in UWP, we have to add the async keyword to the method declaration. And we can’t return a bool: Asynchronous methods can only return a task or nothing. So the method declaration needs to be changed to this:

private async Task<bool> DisplaySavePrompt()
{
    // code here
}

As for the code that displays the Save prompt inside of DisplaySavePrompt, that takes the following form:

ContentDialogResult result = await SavePrompt.ShowAsync();
switch (result)
{
    // Do different things depending on which button the user clicks
}

There’s similar code, with that same async and await keyword usage, with all the file operations as well. And … whatever. It all seems to work, sort of. But the problems I experienced were related to not correctly dealing with asynchronous operations. This is a bit hard to explain, but let me provide the sequence of events that led to the epiphany.

I ran .NETpad and entered a single space into the textbox, changing the value of TextHasChanged to true. Then, I clicked the Open button. Because TextHasChanged was true, I was prompted about saving the document with my Save prompt. If I chose Don’t Save, the Save prompt would close and the Open dialog would appear, as expected. If I chose Cancel, the Save prompt would close and then I’d return to the textbox, as expected.

But if I chose Save, a Save As dialog would appear. And then the Open dialog would appear right on top of it. Not good. These things are supposed to be modal, for one thing. But obviously, the Open dialog shouldn’t appear until we’re done with the Save dialog.

I didn’t see how this could be happening. It’s not possible to perform a file operation without using async and await, so it’s not like I was doing something wrong. I thought.

I was wrong.

I perform file save operations in the Save() and SaveAs() custom methods. Both were originally structured like so:

private async void Save()
{
    // perform asynchronous file operations here
}

So to call these methods, I’d just use code like so:

Save();

Simple, right? Yes, simple. Simple and incorrect. What came to me in my dream was that these methods needed to be called asynchronously too. It wasn’t enough for them to just be asynchronous.

So instead of that Save(); code shown above, I have to call that method like this:

await Save();

But you can’t “await” an asynchronous method that returns void. So I changed it (as I did for SaveAs) to return a task. Now it’s structured like so:

private async Task Save()
{
    // perform asynchronous file operations here
}

And now… it works. In the scenario I noted above, where I add a space and then click Open, the Open dialog doesn’t appear until I’ve finished with the Save As dialog.

It f#$%ing works.

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