Click here to Skip to main content
15,883,921 members
Articles / Desktop Programming / Universal Windows Platform

Programming Windows 10 Desktop: UWP Focus (11 of N)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
7 Dec 2017CPOL20 min read 8.3K   580   4  
Get Started in UWP (moving away from WinForm) Chapter 11 LoadFileFromStorage - When changes from this chapter are complete you'll have an app which saves all entries and loads them from files.

Introduction

This is the continuing journey to discover the feasibility of developing desktop apps via the UWP (Universal Windows Platform).  You can read the previous chapters to get up to speed with what we are doing as we develop our DailyJournal app.

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)[^]
Programming Windows 10 Desktop: UWP Focus (7 of N)[^]
Programming Windows 10 Desktop: UWP Focus (8 of N)[^]
Programming Windows 10 Desktop: UWP Focus (9 of N)[^]
Programming Windows 10 Desktop: UWP Focus (10 of N)[^]

Image 1You can also read the first 8 chapters of the book as a print or kindle book from amazon:

Programming Windows 10 Via UWP: Learn To Program Universal Windows Apps For the Desktop (Program Win10) [^]

Background

If you're starting with this chapter, get the DailyJournal_v019 source (created in the previous chapter) so you can continue along with the changes we make in this chapter.

 

 

FileName Format Resolved

Now that we have our FileName format all taken care of, we can write the code which will load the entries each time a date is selected.

 

This work will be done in our LoadEntriesByDate() method which currently looks like:

Image 2

When we renamed and reformatted our YMDDate value we actually got rid of the YMFolder value because I didn’t think we needed it on the MainPage.xaml.cs.  However, I see that here in LoadEntriesByDate() we are actually incorrectly referencing the YMDDate (2017-12-02) when we really just need the Y-M folder (2017-12). 

Two Ways To Fix This

There are two ways we can fix this and it is instructive to think about which way we might choose.

  1. We could just create a local variable in the LoadEntriesByDate method and call Substring() on YMDDate and set the local here for our use and be done with it.

  2. We could add the YMFolder member variable to the top of the class and set the value every time we initialize YMDDate (down in CalendarView_SelectedDatesChanged).

The choice is almost arbitrary.  However, there are a couple of questions to ask yourself that may lead you to one decision or the other.

1. Will the value be used anywhere else in the class?

Right now, we don’t see the value being used anywhere else, but it may be.  

2. Might it be confusing to another developer in the future if they can’t find the variable and where it is initialized?

I think in this case, since we aren’t using the value anywhere else in the class we can just add it as a local.

Let’s do that now.

Here’s the new line of code and the altered Directory.Exists call using the new local variable as the last parameter.

C#
String ymfolder = YMDDate.Substring(0,7);

if (Directory.Exists(Path.Combine(appHomeFolder.Path, ymfolder)))

 

Image 3

 

If the directory exists we will guarantee that there is at least one entry.  We will guarantee that by deciding a couple of things:

  1. A new YM directory is only created when an initial JournalEntry is created for a particular date.  YM directories should not ever be created unless a file is being saved into the directory.

  2. When a YM directory exists and the last existing JournalEntry is deleted from it the YM directory must also be deleted.  Even though we haven’t written the JournalEntry delete code we are beginning to design how it will work since it is indirectly related to how the load entries code will work.

With those two things in mind, we know that if the directory exists there will be at least one JournalEntry in it.

Pre-Planning Is Designing

It’s always a good idea to pre-plan the things you want your code to do.  

Here’s a summary.

The code we will now write will :

  1. Get the list of all journal entry files for the day selected

  2. Create a new JournalEntry from every file

  3. Add each JournalEntry into the currentJounalEntries collection.

  4. Add the User Interface elements (XAML elements, PivotItem and associated RichEditBox) to the Page so they will be displayed.

  5. Set appropriate RichEditBox.Document to the data found in each journal entry file.

Much of this is very similar to what we do when the user clicks the New Entry button to create a new JournalEntry (CreateNewEntryButton_Click).

Directory.GetFiles()

We need to get all the files in the Y-M folder and we know that the path to that folder is a combination of appHomeFolder.Path and our ymfolder local variable.

We used Directory.GetFiles() to get the files in our JournalEntry class also, but I believe I was remiss in explaining it in detail.

Directory.GetFiles is yet another method provided for us by the .NET library (found in System.IO).

The method, as we are using it, takes three parameters and returns an array of strings which represents the list of files found in the directory.  The parameters are:

  1. Full path to the directory you want to discover files

  2. A file pattern you want to search for (can be *.*, etc)

  3. A SearchOption enumeration value which is one of two values (TopDirectoryOnly or AllDirectories)

To set our parameters properly, we use the Path.Combine to create our search path for the first parameter.

For the second parameter, we use the String.Format() method to provide a pattern based on the current YMDDate. That’s because we only want files for the specific day that the user has selected.  Our String.Format call looks like the following:

C#
String.Format("{0}*.rtf", YMDDate)

This will insure that The Directory.GetFiles() only gets files that match our current YMDDate but which match any file number value.  That way we get the entire list of files for the day selected.

Finally, for the last parameter we set it to only search the TopDirectory since we don’t have any subdirectories to search.

We provide a local array of Strings which the method will use to return all of the filenames into.  

C#
String[] allCurrentFiles = Directory.GetFiles(

                   Path.Combine(appHomeFolder.Path, ymfolder),

                   String.Format("{0}*.rtf", YMDDate),

                   SearchOption.TopDirectoryOnly);

 

Image 4

 

Once Directory.GetFiles() returns the allCurrentFiles will have a Length of 0 (if no files were found) or greater than zero to match the number of files found.

File Naming Scheme Pays Off

The benefit of our JournalEntry file naming scheme is now more evident.

Because we named the files YMDDATE-NUMBER and because of the way the file system auto-sorts the file names we will get them in numeric order meaning that 001 will be added to our allCurrentFiles before 002, etc.  They are naturally sorted for us because our file naming scheme is good.  That’s a nice benefit.

That makes it easy to determine if we have any files in the directory or not, simply by checking the Length property of the allCurrentFiles string array.

Of course, this value must always be greater than 0 in our code since we are guaranteeing that if the ymfolder exists then it will contain at least one file.

We’ll still check the Length value to insure proper execution of the directory listing.

C#
if (allCurrentFiles.Length > 0)

 {

 }

Image 5

 

The code to display the entries will go inside that if statement.

Now, we simply want to iterate through the file list, one at a time.  As we’ve seen in past chapters, the C# foreach construct makes this very easy.

 

C#
foreach (String f in allCurrentFiles)

{

}

Image 6

 

The very interesting thing is that inside that foreach loop, we need to execute functionality that is very similar to what we do in the CreateNewEntryButton_Click method.

However, it is slightly different because we need to load the existing file data into the RichEditBox.  To move forward I’m going to:

  1. copy the code from the CreateNewEntryButton_Click method and run it and examine what it does.  

  2. Refactor out what is different

  3. Attempt to create one method that both methods (CreateNewEntryButton_Click and LoadEntriesByDate) can call.

Let’s copy the code in.  I am literally copying all lines from the CreateNewEntryButton_Click method and pasting it into the foreach loop.

C#
PivotItem pi = new PivotItem();
var entryText = String.Format("Entry{0}", rootPivot.Items.Count + 1);
pi.Header = entryText;
RichEditBox reb = new RichEditBox();
reb.HorizontalAlignment = HorizontalAlignment.Stretch;
reb.VerticalAlignment = VerticalAlignment.Stretch;

currentJournalEntries.Add(
    new JournalEntry(reb,
 Path.Combine(appHomeFolder.Path),
    YMDDate, entryText));

pi.Content = reb;
pi.Loaded += PivotItem_Loaded;
rootPivot.Items.Add(pi);
rootPivot.SelectedIndex = rootPivot.Items.Count - 1;

 

Image 7

This runs and almost works properly.  

Get the Code, Build and Run

Get DailyJournal_v020 and build it and run it.

You will see that the code runs fine and if you have multiple entry files already created for a specific day you will see that a new PivotItem and associated RichEditBox will be created for each.  

However, the issue is that it creates one extra PivotItem than the number of entries you actually have. That’s because in the CreateNewEntryButton_Click we assume that you are adding one to any currently created ones and you must surely have at least one created (the default).

That’s a bit of an incorrect assumption since the user could actually click the [Add] button even though she has not saved anything in the default (Entry1) entry.   

That’s another thing we’ll need to fix.  However, let’s keep going down this path and fix the issue at hand: the problem with one extra PivotItem being added to the UI.

Why The Problem Occurs

The problem actually occurs because we need to load the first entry file we find in the Entry1 PivotItem which will initially be loaded via originally created XAML of the MainPage.xaml.

First Entry File Found Is Handled Differently

That just means for the first entry file we find we do not need to add a PivotItem, but instead just load it into the existing one.

Iterating and Arrays

This leads us to the knowledge that we need to do something different for the first item (first entry file) that is in the allCurrentFiles array.  However, we are using the foreach iterator to iterate through the files and don’t actually know which file index we are on at any time.

First Possible Solution

We could add a counter which is incremented each time through the loop and then we could have a special if statement which only does the special code when the counter is equal 0 (first time through).

But that doesn’t sound great because then we are incrementing and checking a value every time through the loop but we only care about the value one time : when it is zero.

Second Possible Solution

We could also change the foreach loop into a for () loop. A for loop requires are counter and then we index by the counter and there’s a bunch of other work to do which the foreach construct alleviates.

I’d like to keep the foreach but that means I need to somehow :

  1. Use the first value in the array

  2. Remove the first value in the array and then allow the foreach loop to iterate

Internet Search and Research

I did a search and found a simple way to remove the first element from an array so we can use this method which is provided to us on the array object to do this work.  However, once we remove the item from the array we cannot get it back so we need to first use the item.

Skip() Method

The line of code we will implement to remove the item from the array will use the Skip() method.

The final line of code will look like the following:

C#
allCurrentFiles = allCurrentFiles.Skip(1).ToArray();

The Skip() method takes a parameter which indicates the number of elements to skip from the beginning of the array.  We just want to skip the first one so we send in a value of 1.  

Finally, after the Skip method returns, we call ToArray() on the object it returns to cast it back to an array of Strings.

Voila! First Item Gone

We then take that new array which no longer contains the first item and we assign it back into our original array variable.  Just like magic, the first item is gone.

However, we need to use that item first. So let’s write the code now which will use the first file to get the data which we will load into the MainRichEdit Document.

Load From File

As soon as we start working with storage, we know there may be latency in the program so the .NET library methods always run async which means our LoadByEntriesDate() method is now going to have to be marked as async also since it will be calling async methods.

You can change the method signature now so Visual Studio won’t complain at us.

private async void LoadEntriesByDate()

Image 8

 

There are a few things we have to do to read from the existing entry file.

  1. Get the subfolder of the appHomeFolder (Microsoft doesn’t allow you to access these special folders directly for security reasons.  We have to get the app folder then call GetFolderAsync with the folder name.)

  2. Get a handle to the file asynchronously which we will use to open the stream for reading.

  3. Open the file stream asynchronously for reading

  4. Load the RichEditBox Document with the data from the file stream.

  5. Insure the file is closed again (Dispose() the file stream object)

Finally, after we open the file, we will make our call to the Skip() method to insure the first file is removed from allCurrentFiles.

Here’s the code that matches those lines exactly:

StorageFolder subStorage = await appHomeFolder.GetFolderAsync(ymfolder);

Windows.Storage.StorageFile currentFile = 
     await subStorage.GetFileAsync(Path.GetFileName(allCurrentFiles[0]));

Stream s = await currentFile.OpenStreamForReadAsync();           
MainRichEdit.Document.LoadFromStream(Windows.UI.Text.TextSetOptions.FormatRtf, 
    s.AsRandomAccessStream());
    s.Dispose();

allCurrentFiles = allCurrentFiles.Skip(1).ToArray();


Image 9

Path.GetFileName()

Please take notice of the static library method (from System.IO) called Path.GetFileName().  This returns just the file name from a path.  This is important to us in our call to GetFilAsync() because it does not allow the filename that we provide to have any path information in it.  All of the path information has to be set on the FileStorage item as we previously discussed.

Whenever you run Directory.GetFiles() each string it returns will contain the full path and file name.  In this instance we only want the file name so we implement the GetFileName() method for an easy way to get that value.

Build, Run, Test

If you’re following along, run the app and check it out.  Or, get the DailyJournal_v021 source and build it and run.

First Entry Is Loaded

Now, the first entry will be loaded, any time it finds a valid entry file that matches a selected day.

Of course, you have to have a valid entry created for the day you’ve selected.  And, if you have more than one, only the first one will be loaded.  That’s because we haven’t implemented the code for all the other files in allCurrentFiles yet.

Bugs, Bugs, More Bugs

I also noticed that when I attempt to save the Entry1 item that the file is not created.  It looks as if you have to create a new Entry then it will save the entry.   Certainly need to fix that too.

GenerateFileName() Problem

We also have a problem where the current entry does not save back to the proper file. I stepped through the code and discovered that is because every time we load our entries and call the JournalEntry constructor the code actually calls GenerateFileName().

However, when we are loading the files from storage we already have a filename and do not need to generate one.

We need to change the JournalEntry constructor so it will handle this properly.

I’d like it to generate a file name when a new entry is created and to use a passed in file name when the file name is provided.  We can do that by adding another parameter which has a default value of null.

 

To make this change we’ll :

  1. Add another parameter to the constructor signature named FileName and set it to null.

  2. Add the code which determines if the value of the FileName parameter is null and acts appropriately.

 

Here the entire listing of the new JournalEntry constructor:

C#
public JournalEntry(RichEditBox richEditBox,
    String AppHomeFolderPath,
    String YMDDate,
    String EntryHeader,
    String FileName = null)
  {
    _richEditBox = richEditBox;
    this.AppHomeFolderPath = AppHomeFolderPath;
    this.YMDDate = YMDDate;
    this.YMFolder = YMDDate.Substring(0, 7);
    this.EntryHeader = EntryHeader;
    if (FileName == null)
    {
        GenerateFileName();
    }
    else
    {
        this.FileName = FileName;
    }
}

Image 10

Now, all the other code will work the same since the parameter is set to null by default and the GenerateFileName() method will still run.  However, we can now simply change our JournalEntry constructor in our LoadEntriesByDate() method and this issue will be fixed.

Add To currentJournalEntries

But this exposes the other bug which was causing a problem.  We aren’t currently adding the loaded entries to the currentJournalEntries collection.  Since the MainPage.xaml.cs collaborates with the collection class it has no idea which entry is the currently selected entry since we aren’t updating the collaborating class (currentJournalEntries) properly.

Let’s fix that right now and we’ll make sure the constructor to the new JournalEntry passes in the filename.

When we new up our JournalEntry we add the new filename parameter value (Path.GetFileName(allCurrentFiles[0])) and this will be the JournalEntry which is added to our currentJournalEntries collection.

C#
currentJournalEntries.Add(
       new JournalEntry(MainRichEdit, appHomeFolder.Path,
        YMDDate, "Entry1",
         Path.GetFileName(allCurrentFiles[0])));


Image 11

 

This fixes all of the issues we’ve talked about so far, except one.

If you click on a day that has no entries in a month that does contain entries then the AddDefaultEntry() method is not called as it should be.  That’s because we need an else case for those times when the Y-M directory does exist (because there are files for the current month but other days) but there is no current entry files for the day the user clicked on.  

We simply need to add an else statement with the call to AddDefaultEntry() onto the if statement which looks like:

C#
if (allCurrentFiles.Length > 0)

Here’s the entire else statement that should be added:

C#
else
{
    AddDefaultEntry();
}

Image 12

Build, Run, Test

Again, you can build the code and try it out.  Now you can open or create Entry1 and it will be saved properly when you initially save it or when you update it.

It’s beginning to behave like a real app now.

You can get the DailyJournal_v022 source and build and run to try it.

Only First Entry Is Loaded From File

Of course, only the first entry is loaded.  We need to do the same work for each of the other entries.  However, if we simply copy/paste the code down into the foreach block then we have duplicate code that could break.  If it did break it would mean that someone would have to understand that they would then have to fix the code in two different places.

That’s why we want to move this code into its own private method so we can call it from more than one place but know that the code only exists in one place for extending for maintenance.

Since this code basically loads the entry file from storage, I’m going to call the new method LoadFileFromStorage().

New Method: LoadFileFromStorage

We are basically going to :

  1. cut the lines that do that work out of where they currently are

  2. paste them into the new method.  

  3. After that, we’ll make sure we add the necessary parameters the method will need

  4. Then we’ll make add a call to the method in both locations where we need the code to run.

Here are the lines we are going to move to the new method:

 

Image 13

 

Now I’ve created the basic method block and pasted those lines in.  But you can see there are a few issues since Visual Studio is displaying some red squiggly lines.

 

Image 14

There are really three issues with the code, but only two of them does Visual Studio see as problems:

  1. ymfolder is not defined in this method’s scope (it is local to LoadEntriesByDate)

  2. allCurrentFiles string array is not defined in this method’s scope either (it’s also local to LoadEntriesByDate).

  3. MainRichEdit is accessible but is only correct when the Entry1 is showing.  On other generated entries, we need the generated RichEditBox.

All of these issues can be solved easily by passing in values as parameters.

Let’s alter our method’s signature to take the parameters we need so we can pass the values* in.

Note: values isn’t quite correct here because we are also passing in an object reference (RichEditBox).  

Here’s the new method signature which will resolve all three problems.  

However, we’ll need to change two things in the method’s code too:

  1. Paramter to GetFileAsync(Path.GetFileName(allCurrentFiles[0]) to entryFileName parameter

  2. the reference to MainRichEdit.Document... to reb.Document…

C#
private async void LoadFileFromStorage(String ymfolder,
           String entryFileName,
           RichEditBox reb)

 

Here’s what the altered code looks like now:

 

Image 15

I named the first parameter,  ymfolder, exactly as it is named in the calling method.  That just made it simpler so I didn’t have to change the line of code that accesses that value.

The second parameter is named entryFileName in an attempt to show that we want only the filename passed.

Now, we need to add our calls to this method up in the two places in LoadEntriesByDate().

First Call To LoadFileFromStorage

The first call will go right where the previous block of code was located.

C#
LoadFileFromStorage(ymfolder,
                       System.IO.Path.GetFileName(allCurrentFiles[0]), MainRichEdit);

You can see that we strip off any path information in the call so that the receiving method will only get the filename itself.

Also, in this case we send in the MainRichEdit RichEditBox because this is the first entry.

 

Image 16

Second Call To LoadFileFromStorage

We will place our second call to LoadFileFromStorage within the foreach loop after the RichEditBox named reb is created (because we need to send it in as a reference to the LoadFileFromStorage method).

C#
LoadFileFromStorage(ymfolder, System.IO.Path.GetFileName(f), reb);

Image 17

 

In this case we reference the ymfolder again and we strip the path information off of our temporary foreach variable (f) which represents the entry file name.

And, of course, we pass in the newly created reb so the proper entry document will be filled with the data from the entry file.

That’s it.  

Build and Run : All Existing Entries Load

Build and run the app and try it out.

Get the DailyJournal_v023 and build it and run it.

Source Code At Github Repo

You can also get all the source at the Github repo : https://github.com/raddevus/Win10UWP

You can now:

  1. Save one or more entries on any date

  2. The app will reload all entries and associated data any time a date is selected.

Two Simple Scenarios To Test

  1. If you have 8 entries created on 2017-12-22 and you click on that date, all of the entries will load and each one will load its associated data.

    1. You can then click on each entry and view the associated document.

  2. If you click on a day which contains now entries, you will see the empty default entry.

    1. If you add data and save the new file will be saved so that if you move away to another date and move back the associated entry file will be loaded into the RichEditBox.

App Crash

As I was using the app I noticed the app crashed a couple of times, but it was difficult to determine why.  I ran it in debug and clicked around, changing dates.  I finally noticed that the app would crash in the MainCalendar_SelectedDatesChanged event handler on the following line:

C#
YMDDate = MainCalendar.SelectedDates[0].ToString("yyyy-MM-dd");

Very Odd Behavior

For some reason the control’s SelectedDatesChanged event fires but it thinks that no date is selected at all so the SelectedDates[0] is null. It’s as if the control believes you clicked the control but between dates or something.  Not sure.  However, the attempt to call ToString() on a null object obviously is undefined and the app throws an exception or crashes.

I fixed that in the code  with a check for null on that value with a check on the Count property of the SelectedDates object.

C#
<s>if (MainCalendar.SelectedDates[0] != null)</s>

ARTICLE EDIT - Previous line of code should be:

After posting this article I learned that the previous fix wasn't quite right.  I updated the source in the attached zip (v023) and at the Github repo.  It should actually check for a Count > 0 -- checking for null will still throw the error and crash the app.

C#
if (MainCalendar.SelectedDates.Count > 0)

If you find that you click on a date the app seems to ignore your clicks on the MainCalendar it could be because it thinks the SelectedDates[0] is null.  It’s odd and I’ll research it further.

Next Time

We’ve almost completed the basic app, however, we never have updated the code for the ListView control yet.  The ListView is supposed to provide the list of dates which have entries and a count of entries that exist for each date.  

Here’s a snapshot of my old WinForms version of this:

Image 18

This allows the user to click through the months, see what days have entries associated with them and then choose them (move to the entry) by clicking on the ListView item.

History

2017-12-07: first publication

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) RADDev Publishing
United States United States
"Everything should be made as simple as possible, but not simpler."

Comments and Discussions

 
-- There are no messages in this forum --