Introduction
Please check out the previous entries to get up to speed with our ongoing project to create Daily Journaling app via UWP.
Programming Windows 10: UWP Focus (1 of N)[^]
Programming Windows 10: UWP Focus (2 of N)[^]
Programming Windows 10: UWP Focus (3 of N)[^]
Programming Windows 10 Desktop: UWP Focus (4 of N)[^]
Programming Windows 10 Desktop: UWP Focus (5 of N)[^]
Programming Windows 10 Desktop: UWP Focus (6 of N)[^]
Background
This is my continuing attempt to tell the story of Windows 10 Desktop Development via the UWP (Universal Windows Platform). This time, after we overcome some XAML challenges, we delve into saving our files into the file system.
Now that we can create new journal entries for each day, we need to provide a way to save the data that the user types or pastes into the RichEditBox.
Organizing Our Journal Entry Files
I’d like to organize the files so that each month’s entries are in the same folder. In the past I’ve created what I call the Y-M
directory Year-Month which looks like 2017-11. This format sorts nicely in File Explorer and allows us to find the physical file entries later very easily.
Inside each month folder we will need a scheme for naming each file that is created. This time we will use Y-M-D-N
which will represent Year-Month-Day-EntryNumber. Since there can be one or more entries each day, we need a way to differentiate them and keeping a counter number at the end of the file name makes things very easy and keeps everything organized.
Now all we need to do is :
After we provide a way for the user to launch the action she wants to take we’ll need to write the code which actually saves or deletes the associated file. If you’ve the File System in the past under WinForm development and this is your first time using it under the UWP paradigm you will learn (as I did) that everything is different.
First, let’s update our XAML to add our new AppBarButtons.
DailyJournal_v006.zip
If you don’t already have the code from the last article, go ahead and download it and open it in Visual Studio.
Adding New Buttons: Copying Previous Code
To add the new Save and Delete buttons I’m simply going to copy the code from the previous button we added (CreateNewEntryButton) and altering it.
However, we will have to be careful to not reuse the x:Name or Click values, of course, because those have to be unique to every button.
I’ll add the new Save button first and to do that, I’ll:
-
Copy the entire AppBarButton
tag
-
Paste right after the closing tag of our existing AppBarButton
-
Change the name to SaveEntryButton
-
Delete the Click
attribute and its value entirely
-
Change the icon to Save (a disk icon - please refer to https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.symbol for all icons)
-
Change the Label to “Save”
-
Change Tooltip (more about tooltips later) to "Save (Ctrl+S)"
The final XAML that you can copy / paste into your file is:
<AppBarButton
x:Name="SaveEntryButton"
Icon="Save"
Label="Save"
ToolTipService.ToolTip="Save (Ctrl+S)" >
</AppBarButton>
When you add that XAML you will see the button appear in the DesignView.
After you add the XAML and see the Save (disk) button appear, go ahead and double-click it to add a new Click event handler to your MainPage.xaml.cs.
When you do, Visual Studio will add the handler and open up MainPage.xaml.cs for you.
Determine Which RichEditBox Is Selected
The first thing we need to know so we can save the data is which RichEditBox is currently displayed? However, to determine which RichEditBox is currently selected we need to add some event handlers so when certain actions occur we store the current visible RichEditBox so we can access it and use it.
XAML / User Interface Clean Up
To do that work we need to clean up a few things in our XAML because currently we have an extra PivotItem we do not need. We need to get rid of the second PivotItem in our XAML: the one which contains the MainRichEdit2.
Our final app will initially display only Entry1 each day (not Entry2) so this PivotItem is not needed and is only going to confuse us so let’s get rid of it.
Delete PivotItem XAML
Let’s do that now. Select it and hit your [Delete] key.
Setting Focus
The next thing we want to do is insure that whenever our first PivotItem is Loaded or obtains focus (user is interacting with it) then we set focus onto the RichEditBox. We want to set focus to the RichEditBox because that is how the user actually wants to interact with our app. He doesn’t actually care about the PivotItem, because the PivotItem is simply a means to get to the RichEditBox. Since this is true, we need to insure that whenever the user clicks a PivotItem header focus gets set to the associated RichEditBox.
Outer Container
Also, keep in mind that when you click on a PivotItem’s Header you are actually first clicking on the outer container : the Pivot. The Operating System propagates your click on the Pivot down into the PivotItem (inner container) but the way to set the focus on the associated RichEditBox is to add a handler so that any time the outer Pivot is clicked we check which PivotItem is clicked and then set the focus to its associated RichEditBox.
When the outer Pivot item is clicked the GotFocus event handler is actually called. It is not, as I first assumed also, the Click event.
This is a lot of work isn’t it? This is what developers do. This is why we have to understand all these crazy little details.
Add GotFocus Event Handler
Let’s Add the GotFocus handler to our Pivot.
We’ve added events to XAML elements before but let’s go through it again.
-
Click the Pivot
element in the XAML (while viewing MainPage.xaml
)
-
In the Properties
window (usually underneath the Solution Explorer on the right), click the [Lightning bolt] icon to display a list of Event Handlers that this control supports.
-
Scroll down in the list until you see the GotFocus
Event.
-
Double-click on the TextBox next to the GotFocus
event.
When you double-click on the GotFocus TextBox, Visual Studio will add the Event handler to your XAML and add the new method to MainPage.xaml.cs.
This is the easy way to add an event handler because Visual Studio helps you by doing most of the work for you. In a moment we’ll see how we can add our own event handlers via C# code.
Add Code To Event Handler: Set Focus On RichEditBox
Let’s think about what we want the app to do when the user clicks on the PivotItem (via clicking the Pivot).
Here’s the code to do that work. It requires that we obtain references to all of the affected objects (Pivot
, PivotItem
, RichEditBox
).
Pivot p = sender as Pivot;
PivotItem pi = p.SelectedItem as PivotItem;
RichEditBox reb = pi.Content as RichEditBox;
reb.Focus(FocusState.Keyboard);
I’ll use the line numbers from Visual Studio to explain what the code is doing.
When the rootPivot_GotFocus
event fires it provides two arguments. In our code we don’t need to use the second argument but we do use the first one (sender
). Since we know that GotFocus
was called from a Pivot element we know that the sender object will be a Pivot
, however, we need to convert the generic sender
object to the type we know we need to use. We can do this easily with the as keyword and that is what we are doing here on line 65.
Now that we have a valid Pivot
we can use it to get to the PivotItem
that it contains on line 66.
The currently selected Pivot
contains a SelectedItem
property which is the associated PivotItem
so we get that and store it in our local PivotItem
(pi
) variable.
All For RichEditBox Reference
We do all this work just so we can get to the RichEditBox
that is contained inside the PivotItem
. We know that we originally set our PivotItem
’s Content
property to b a RichEditBox
so once again we use the as keyword to get to the associated RichEditBox
on line 67.
Finally, we have a valid RichEditBox
. This was the whole reason we went through all of this so that on line 68 we can set the keyboard focus to the RichEditBox
.
Download Code and Try It
Now, if we run the app and click on the Entry1 (Pivot) then the I-beam cursor will automatically appear in the RichEditBox
and the user can begin typing immediately.
Get the DailyJournal_v007.zip
and build the code, run it and click on Entry1
and you will see that is true. Also, now when you add a New Entry, that Entry will receive focus.
Did You Recognize the Existing Problem?
However, you should’ve noticed there is still a problem.
The app should probably go ahead and set focus to the RichEditBox
immediately when it loads up. To fix that however, we need to add a Loaded event handler to the PivotItem
.
A Bit Confusing
This is a bit confusing because we’ve been working with the Pivot
, but now we need to add an event to the PivotItem
.
You may have thought (as I previously did) that you could just set focus to the RichEditBox
when the MainPage
has loaded. However, you cannot be sure the PivotItem itself has been loaded and if it has not then the RichEditBox
would not be loaded and that would cause the app to crash.
Let’s go add the Loaded event handler to our PivotItem
which exists in the XAML. After that we’ll begin to see how to add events to the programmatically added elements (PivotItems
and associated RichEditBoxes
).
Adding the Loaded Event Handler To the PivotItem
It’s the same work on a different element to add the Loaded event handler.
-
Click the PivotItem
element in the XAML (while viewing MainPage.xaml
)
-
In the Properties
window (usually underneath the Solution Explorer on the right), click the [Lightning bolt] icon to display a list of Event Handlers that this control supports.
-
Scroll down in the list until you see the Loaded
Event.
-
Double-click on the TextBox next to the Loaded
event.
The code we will add to our PivotItem Loaded event will be very similar but will not require us to get our PivotItem reference from the Pivot. Instead, we will have a reference to the associated PivotItem passed to us via the sender object (first parameter of the PivotItem_Loaded method.
Here’s the code we will add to the Loaded event:
PivotItem pi = sender as PivotItem;
RichEditBox reb = pi.Content as RichEditBox;
reb.Focus(FocusState.Keyboard);
This demonstrates that the sender
object becomes whatever type which initiated the firing of the event. In this case, when the PivotItem Loaded
event handler fires the sender is a PivotItem
.
So in this case we just need to get the PivotItem’s Content (RichEditBox) so we can set focus on it.
When A PivotItem Loads: Set Focus to RichEditBox
Now, when we run the app the RichEditBox will get focus immediately (even before the user Clicks the Pivot). This will also insure that whenever a PivotItem loads that it’s associated RichEditBox gets the focus. That is important since it is the way the user will actually interact with the app.
Get Code and Try It
Go ahead and build and run it if you’re following along. Or download DailyJournal_v008.zip and build and run and try it.
Results of Loaded Event
Now, even before you click the Entry1 PivotItem header, you should see the I-beam cursor in the RichEditBox.
However, there are still issues which will cause user interaction errors.
What Problem Continues to Exist?
It’s a bit tricky because if you start the app and click the New Entry button you will see that the RichEditBox on the new Entry will get focus. However, that is only because you had focus in the Entry1 RichEditBox and we get lucky.
Recreate The Problem
If you first click on the CalendarView control (set focus on CalendarView) and then click the New Entry button you will see that the RichEditBox does not get an I-beam cursor because it does not get focus. This will cause us issues going forward.
Why Does This Issue Occur?
This problem occurs because the generated PivotItem
’s and associated generated RichEditBoxes
do not have the event handlers activated. Keep in mind that we added the event handlers to the specific PivotItem
that exists in the XAML, but a new PivotItem
and RichEditBox
is generated every time we click the New Entry button. We need to add event handlers to the generated objects so they will work properly.
Let’s go fix that.
Back to CreateNewEntryButton Method
To Fix this problem we need to add an event handler to every PivotItem
that is generated when the user clicks the New Entry button.
It’s not difficult, but it is kind of tricky to find if you’ve never done it before.
As a matter of fact, it’s only one line of code because we’ve already actually written the method (PivotItem_Loaded
).
First, take a look at the current CreateNewEntryButton_Click method to get our bearings:
Showing All Code
I’m also showing the rest of the code because we are going to use the PivotItem_Loaded method to fix our problem and I wanted it to be visible.
Examine my listing of the CreateNewEntryButton_Click closely and you’ll see a empty line at 59 (Visual Studio line numbers shown in image).
That is where I’m going to add the line of code to fix our problem.
We need to tell every generated PivotItem (pi) that it should register the event handler for the Loaded event. We also want to tell it that the event handler method will be the method we already wrote : PivotItem_Loaded.
If you begin to type on line 59 and start typing pi.L you will see that Intellisense will try to help you.
When Intellisense
pops up you’ll see a list of Event Handlers ([lightning bolt] icons) and Properties
(wrench icons) and (not shown here) methods (functions) (box
icons) that are supported by the object.
In our case we want the Loaded event, so if you want you can double-click that item in the list and it will be added to your code. The value that Loaded is set to is a delegate (method that will be called when the Loaded event fires). The syntax is a little different because we are adding our method to any other methods that might be already there so we use a +=
to do this.
Here’s the full line of code :
pi.Loaded += PivotItem_Loaded;
Problem Fixed
That’s it. Now the problem is fixed. Whenever you add a new journal entry the Loaded event will set the focus on the RichEditBox.
Even if you click on another control such as the CalendarView first and then click the New Entry button you will find that the RichEditBox gets focus.
Build, Run, Try
Go ahead and build, run and try it out. If you need the code, get the DailyJournal_v009.zip
at the top of this article.
Why Have We Went So Far Afield?
All of this work has been to allow us to get to a place where we are able to:
- determine which RichEditBox is currently in use so that we can save its data.
To do that, we have to know when the RichEditBox
is activated. To know when the RichEditBox
is activate requires the code we’ve written to handle the Loaded
and GotFocus
events.
Capture Currently Selected RichEditBox
Now we need to capture the currently selected / activated RichEditBox so that when the user clicks the Save button we will get the data that is associated with the RichEditBox which is currently being viewed by the user and save its data.
New Member Variable
That is very easy to do simply by doing two things:
-
adding a new private member variable to our MainPage
class
-
Setting the member variable to reference the currently activated (focused) RichEditBox
Since this is a RichEditBox
we simply add the following code to the top of our MainPage
class:
private RichEditBox currentRichEditBox;
Note: You probably already have that in your code, if you’ve downloaded previous source zip files because I had been figuring out how to do that work and I accidentally left my changes in there. It’s fine, it won’t hurt anything, just saves you some typing.
Once you’ve added that line we need to set this reference any time the RichEditBox
is gets focus. Right now that happens in two different places :
-
rootPivot_GotFocus method
-
PivotItem_Loaded
Actually, now that I’m looking at those two methods I see that I’m repeating some code. That breaks one of the SOLID software development principles (SOLID (object-oriented design) - Wikipedia[^] ) known as DRY (Don’t Repeat Yourself) so I will refactor the code out into its own method so we only have to set our currentRichEditBox
reference in one place.
Here’s the changed C# code:
private void rootPivot_GotFocus(object sender, RoutedEventArgs e)
{
Pivot p = sender as Pivot;
PivotItem pi = p.SelectedItem as PivotItem;
RichEditBox_SetFocus(pi);
}
private void PivotItem_Loaded(System.Object sender, RoutedEventArgs e)
{
PivotItem pi = sender as PivotItem;
RichEditBox_SetFocus(pi);
}
private void RichEditBox_SetFocus(PivotItem pi)
{
RichEditBox reb = pi.Content as RichEditBox;
reb.Focus(FocusState.Keyboard);
currentRichEditBox = reb;
}
The first two lines in the new method were previously in both the Loaded
and GetFocus
method and that isn’t any good.
Now, I’ve simply taken those lines and moved them to the new method. Of course I also had to add the reference to the PivotItem as a parameter so I could use the object in my new method.
Then I also added the call of the new method to both of the methods (GetFocus
and Loaded
) so everything will still function the same.
Added Line of Code To Capture RichEditBox
Finally I added the last line of code to the new method so no matter how the RichEditBox
gets activated that we will get a reference to it so we can use it to get the data and save it to a file.
However, the code doesn’t do anything different yet, because we haven’t implemented the Save button.
Let’s go and double-click our Save button so we can add an event handler to it so we can write some code when the user clicks the Save button.
Add SaveButton Click Event Handler
Let’s do this the same way we added event handlers for GotFocus and Loaded.
Open up the MainPage.xaml
and :
-
Click the SaveEntryButton element in the XAML.
-
In the Properties
window (usually underneath the Solution Explorer on the right), click the [Lightning bolt] icon to display a list of Event Handlers that this control supports.
-
Scroll down in the list until you see the Click
Event.
-
Double-click on the TextBox next to the Click
event.
Of course, when you double-click the TextBox Visual Studio opens MainPage.xaml.cs and adds the new method.
Long Chapter: Quick Save
Since this chapter is becoming so long we will go ahead and make the Save button save the text to one file for now. That means whichever RichEditBox that is currently displaying will determine the data that is in the file. If you switch to another RichEditBox and save again it will overwrite your previous data.
Future Functionality
Then, next time we will begin to solve the real problem storing data multiple files and then reloading the files into their associated RichEditBoxes.
File Storage : Saving Data
Here’s the code we are going to use in our SaveEntryButton_Click method:
Windows.Storage.StorageFolder storageFolder =
Windows.Storage.ApplicationData.Current.LocalFolder;
Windows.Storage.StorageFile sampleFile =
await storageFolder.CreateFileAsync("FirstRichEdit.rtf",
Windows.Storage.CreationCollisionOption.ReplaceExisting);
I got that code from a sample at :
https://docs.microsoft.com/en-us/windows/uwp/files/quickstart-reading-and-writing-files
I changed the name of the file that it saves but the rest is the same.
When you add the code into MainPag.xaml.cs
Visual Studio Intellisense
is going to warn you about a problem.
It is telling us because we are calling an asynchronous method (CreateFileAsync
) from a synchronous method (SaveEntryButton_Click
).
This is similar to what we saw way back in chapter 2 when we tried to pop up a dialog box.
We can fix this without help from Intellisense
. Simply add the keyword async
to the SaveEntryButton_Click
method (right after the private
keyword).
As soon as you do that you will see that Intellisense
stops bugging you.
That creates the file, but it doesn’t actually save any data. To do that we need to :
-
Create a IRandomAccessString
when opening the File asynchronously
-
Call the RichEditBox Document.SaveToStream
-
Close (Dispose
) of the Stream
Here’s the code you need to add:
IRandomAccessStream documentStream = await sampleFile.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite);
currentRichEditBox.Document.SaveToStream(TextGetOptions.FormatRtf, documentStream);
documentStream.Dispose();
That first line is another Async call, since the app doesn’t know how long it will have to wait to get disk access and it doesn’t want to freeze up.
Once you have a valid Stream
which we can use to write the file data to, we can call the SaveToStream
method. Notice that the first parameter is an SDK (Software Dev Kit) enumeration which tells the method to save the file as standard RTF (Rich Text Format). That way, if you have any images in there, it’ll save those too.
Finally, we Dispose
of our stream to close the file and we are done.
Get the Code and Try It Out
Build the code, run it and type something in the RichEditBox and click the Save button. If everything works properly you won’t really even know it saved.
But, Where Is The File Saved?
Ah, the $64 dollar question. Now that we use the special Windows.Storage.ApplicationData
and it’s LocalFolder
value, the app lets the OS tell it where it should save the file.
Once again this is because the UWP architects are attempting to insure that no matter what device the app is running on it should not fail to save the data.
In our case, on the desktop, you can find the file by going to :
%localappdata%\Packages\116d1010-a1e4-452a-a1d2-c84aea07af6d_gw4zt26480tv8
You should be able to copy that last line, paste it into File Explorer and it will take you to that location.
Isn’t that bizarre?! Well, %localappdata%
is an environment variable which points to your Windows SpecialFolder
LocalAppData
which is generally found at: c:\users\<username>\local\AppData\local\
Underneath there in the \Packages
folder is where UWP apps store their data.
Here’s the file sitting in my directory:
Package.appxmanifest
The last item is a GUID (Globally Unique ID) which is generated by a Visual Studio tool to uniquely identify your app when you first created it.
As a matter of fact, if we go back to Visual Studio and double-click the Package.appxmanifest
in Solution Explorer and then choose the Packaging section, you will see that the Package
family name contains this value.
If you save some data in your file, you can navigate down to the file in the file system (using the previous clues and take a look at it using MS-Word or WordPad.
Here’s the data I saved (which even includes a snapshot image).
I then opened that document in WordPad:
NOTE: Windows 10 thinks the image is dangerous so if you save an image in there it may not want to allow you to view that data.
I leave you with that, since this is a ridiculously long chapter.
However, I’ll be back soon to fix the file saving schema and the add the ability to delete entries (along with their files).
History
2017-11-29 : First publication