Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / UWP

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

5.00/5 (4 votes)
6 Dec 2017CPOL22 min read 13.9K   405  
Get Started in UWP (moving away from WinForm) Chapter 10 Refactoring sometimes means fixing bugs -- Getting the DailyJournal FileName format working.

Introduction

Nine chapters precede this in depth look at the feasibility of developing apps via UWP.  Take a look at the previous entries to get up to speed with the DailyJournal app we are building as I lead you with specific details and numerous screen shots.


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)[^]

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

We had a number of problems (bugs) in the app when we ended the last chapter (See the previous chapter section, Other Problems : Bugs In The App for the list).  

The first item listed there describes the problem that the Entry1 entry will never save.  Let’s try to fix that first.  

If you need to get the code so you can start where this chapter begins, go ahead and get the DailyJournal_v017.zip.

Why Doesn’t Entry1 Save?

Understanding why Entry1 doesn’t save helps us solve the problem.  It is never saved because it is never added to the JournalEntries collection.  Since it is never added, it can never be found in that list when the user clicks a calendar date.  

When Will A Blank Entry1 Appear?

Once we add the functionality to load previously saved entries a blank Entry1 will only occur when there are no entries for the day which the user selected.  If there are any entries (files) then they will be loaded in order and the Entry header will be incremented starting with 1.

This means the only time we will have a blank Entry (in the future) is when there are no other entries created for the day.  This is code would run in the else statement of LoadEntriesByDate() -- a method we wrote in the last chapter but didn’t completely fill out.

Add A Default Entry

I am going to call the empty Entry1 the default Entry so I am going to to call the method that does that work AddDefaultEntry().

Let’s go write that method now.

AddDefaultEntry Method

Open the MainPage.xaml.cs file and move down to the LoadEntriesByDate() method.

We are going to work in the else portion of the code, since we only want our code to run when there are no entries for a date.

I’m going to add the call to the method into that else block, even though we haven’t written it yet.

C#
AddDefaultEntry();

Image 2

 

Now, let’s open up some space in the file right after the LoadEntriesByDate() method and write the new method.

The code in the method is only one line long:

C#
private void AddDefaultEntry()

{

   currentJournalEntries.Add(new JournalEntry(MainRichEdit, appHomeFolder.Path,

              YMFolder, "Entry1"));

}

Image 3

 

All we have to do is add a new JournalEntry to our currentJournalEntries collection.

Hardcoded Values : Another Sniff Test

The last parameter is always “Entry1” so we’ve hardcoded it into the call.  This hardcoding doesn’t really pass the sniff test.  It seems to smell a little because something seems strange here.

This is not a good practice since this can create magic strings in your app: else that something depends upon which aren’t readily known by other developers who may look at your code.  We’ll leave it that way for now, but later we’ll talk about how we could make this better.

Keep in mind that the the LoadEntriesByDate() method is called any time the selected date changes because we have implemented a handler for that event ( MainCalendar_SelectedDatesChanged)  which calls LoadEntriesByDate().

 

Image 4

 

Builds, Runs, But Doesn’t Quite Work Right

Now that we’ve made those changes the code will build and run, but it doesn’t work exactly as it should.  That seems to be the story when refactoring.  That’s because I discovered a bug -- which is related to wrong-thinking about how the LoadEntriesByDate() should work.  

The if statement is actually incorrect.

Right now the if statement is checking to see if the YMFolder exists.  If it does then it will load all the files. However, the if statement should really check if any files match the pattern for the selected date.  Then if it does it should load any files.  We need to change that but we need to talk a bit about the pattern we will use for file names too.  And since we need to deal with how the files are going to be named let’s go ahead and tackle that code now.

How Will DailyJournal File Naming Work?

The format of the DailyJournal file names will use the following pattern:

Y-M-D-N.rtf where

  • Y = 4 digit year

  • M = 2 digit month

  • D = 2 digit month

  • N = 3 digit generated entry number

  • Extension will always be rtf (rich text format)

 

It will look like :YYYY-MM-DD-NNN.rtf or 2017-12-03-001.rtf

 

Every time a new file is created (saved the first time) the JournalEntry class will generate an appropriate file name to match the pattern.

 

Let’s go take a look at the JournalEntry class and its constructor to determine if it has enough information to generate a filename when it is constructed.

More Cleanup

As soon as I take a look at the JournalEntry class I recognize some poorly named member variables and remember that in our last chapter we changed how some things worked but we never changed the member var names.  Let’s do that now and we’ll also see how Visual Studio can help us rename variables using its refactoring functionality.

YMFullPath Should be AppHomeFolderPath

YMFullPath no longer points to the full path since we actually pass in the Y-M folder seperately.

Let’s rename it to represent the value it actually holds.

Visual Studio Rename Help

Right click on the member variable at the to of the class and and menu will appear which provides a choice of [Rename…].

 

Image 5

 

Select the [Rename…] choice and Visual Studio will highlight the member everywhere it appears in the file and it will display a helper dialog which will allow you to rename places where the text is found even in comments, etc.

 

Image 6

 

Once that appears, just type your new name (AppHomeFolderPath) and Visual Studio will begin to change them all.  Once your done press <ENTER> or click the [Apply] button and all will be renamed properly.

The benefit in doing that is when that variable is referenced in other files also you don’t have to worry about searching through every file yourself.

 

Image 7

 

As you can see we still have the constructor parameter named improperly so you can rename it also yourself.  I’m going to rename mine now to the same thing as the member variable.

After you do the rename, it’s a good idea to rebuild just to make sure everything is correct and you haven’t introduced new variables that aren’t quite right or something.

Now that the variable names make more sense we can get back to adding our code which will name the files properly.  

This will be an interesting problem because of the counter value at the end of the file name.

 

Yes, It’s Another Refactor

However, as I examine the JournalEntry constructor I notice that we do not have the day part of the date.  The JournalEntry requires this for creating the filename properly so now I’m thinking about the constructor parameter YMFolder.  That is passing in the Y-M value.  I could now add another constructor parameter that passes in the day value.  That would be kind of easier and less refactoring but it would also be sloppier.  

 

What we really need to do is pass in the formatted Y-M-D string in one parameter and then have our JournalEntry use it for the Y-M directory and to create the day value on the file.  As a matter of fact, if we pass in the full Y-M-D value we will have the file name just as we want it and we’ll just need to add the N value for the file number.  Let’s go and refactor the JournalEntry constructor again and then we’ll change the calls to the constructor too.  We’ll have one other thing we need to change in the MainPage class also because it stores the YMFolder and we’ll change that to store the YMD value.

 

Before we start this refactor, let me give you the code that I just changed (earlier in this chapter) so we can make sure we have the same code base to start with.

Download DailyJournal_v018.zip and start there.

 

Add New Member Variable : YMDDate

  1. The first thing I am going to do is add a new private member variable to JournalEntry which is a string and named YMDDate.

  2. Next I’m going to rename the parameter, YMFolder, in the JournalEntry constructor to YMDDate.

  3. I’m going to make sure the constructor code initializes the YMDDate member with the YMDDate parameter.

  4. Finally, I’m going to set the value of our old member variable YMFolder by getting the value from the YMDDate string parameter in the constructor.  I am doing this because the YMFolder is still used on its own and we need that value separately from the YMDDate -- for the folder name where the files are stored.

You can see all of these changes in the following code listing:

 

Image 8

Substring Method

Line 30 is the most interesting line.  Since we know the format that the YMDDate will come in, we can simply get the Y-M off of it by simply calling the built-in String method called Substring().

 

Substring() takes two parameters in this case. The first parameter is the index in the string where we should start.  In our case we want to start with the first character in the string and arrays are indexed by zero in C# so we provide it with a zero.  

 

The second parameter is the length (or number of characters) you want to get from the string starting at the index.  We want 4 for the 4 digits of the year, 1 for the dash and 2 for the 2 digits of the month, which is 7 total.

We take that and set our YMFolder member variable and now it will be set just as it was previously.

 

What If We Build Now?

The code will build but it isn’t right.  The code will still build because we have not changed the number or type of parameters the constructor takes.  Previously the constructor’s third parameter was a string and now it is a string so to the compiler nothing has changed.

However, we know that we are only passing the Y-M portion of the string and it’s not going to work properly in the future.  

 

Let’s go change the code in MainPage.xaml.cs to fix that problem.

Now I’m renaming the MainPage member variable YMFolder to YMDDate.

This value is mainly used in each place where we construct a new JournalEntry.

However, it is also currently initialized in the MainCalendar_SelectedDatesChanged event handler.

Image 9

 

We just need to slightly change that format string to add the last dash and the two digit day value.

Here’s the line of code we will replace the current line with:

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

Image 10

 

All of this builds with no errors and it will run and work just as it previously did, however, we are much closer to generating our new file names properly with this code.

I haven’t Forgotten About LoadEntriesByDate()

Keep in mind that we still do need to fix the MainPage LoadEntriesByDate() method because it is improperly checking for the Directory.Exists() right now, but I haven’t forgotten and we will fix that.  But again, once we refactor our JournalEntry to properly generate file names that work will be easier so let’s continue down this path.

Solve The File Name Generation Problem

When the JournalEntry is constructed we need to go ahead and generate a file name for the class also.  That way if the user decides to save the file, the Save() method will use that file name when it writes the data to storage.

File Name Is Different Than File

Notice that when we instantiate the JournalEntry we generate the file name and not the file. We only create the actual file in storage if the user does the save. The point here is that when the object is constructed it has everything generated that it needs to do its work.  The file name may never be used if the user doesn’t save the file and that is okay.

The file name will only be used within the JournalEntry class so will add it as a new private member variable.

Here’s the one line of code to add it as a member:

C#
private String FileName;

Image 11

 

Notice also that we do not have to add this item to the constructor because it will be generated by the JournalEntry class itself.  

Since the code to generate the file name promises to be more than one or two lines I will separate it out into it’s own private method named, GenerateFileName() and I will add a call to that method as the last line in the JournalEntry constructor.

Stub Out Methods

I usually stub out my methods like this just to get the code going but still building okay since there are no syntax errors or other others. “Stub out” just means to add the basic method declaration even though it doesn’t contain any code.

 

Image 12

 

Our GenerateFileName method needs to :

  1. Determine if there are any existing files for the particular day.

  2. If there are no files for the current date, then it will set the file number to 001 on the end of the FileName member variable.

  3. If there are files for the current date then it needs to get the max N value and then increase that by one and set the value in the format of 00X on the end of the FileName. (This is not  perfect algorithm, but it’ll work for our purposes)

 

Here’s the code I wrote for the case when there are no files currently created for the day.

 

C#
string targetDirectory = Path.Combine(AppHomeFolderPath, YMFolder);

 if (Directory.Exists(targetDirectory)){

     String[] allCurrentFiles = Directory.GetFiles(targetDirectory,

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

     if (allCurrentFiles.Length <= 0)

     {

         FileName = String.Format("{0}-{1}.rtf", YMDDate, 1.ToString("000"));

     }

     else

     {


     }

 }

 else

 {

     FileName = String.Format("{0}-{1}.rtf", YMDDate, 1.ToString("000"));

 }

Image 13

 

Let’s change our Save() method to reference the FileName member instead of the hardcoded value (FirstRichEdit.rtf).

 

The current Save() method looks like:

Image 14

 

We just change that one parameter to reference our new member variable.

Image 15

 

Finding Bugs Is A Part of Refactoring

There is another bug and this time it is in the Save() method.  When we renamed the YMFolder items we accidentally renamed two YMFolder variable which should’ve stayed as YMFolder.

The YMDDate on line 62 should really be the YMFolder and the YMDDate on lines 64 and 66 should also be changed to YMFolder.  That’s because of the way we have to create the Y-M folder under the localstorage folder.  Here’s the updated code so you can see that I’ve renamed those two items.

Image 16

 

Why An App Crashes Isn’t Always Obvious Why

At this point I built the app and ran it to test to see if it would save a file properly.  There are cases where it will, but since we have this empty else statement in our GenerateFileName() method there are times that the app will actually crash.

It will crash when the Y-M directory exists and there are files in the directory.  If that occurs, the JournalEntry FileName will not get set (it will be null).  Then when you go to save the associated file, since it hasn’t actually been set, the app will crash because it cannot create a null file.

 

That makes sense when you think about it, but when you run the code and it crashes it isn’t obvious.

 

Let’s fix that problem by filling out the else statement with the code that we need to generate a valid FileName when there are already entries created for the particular chosen day.

Algorithm Assumptions

This algorithm is going to assume that the entries are number from 1 to N with no missing values in the sequence.  The problem with this is when a user deletes a particular entry from a set of existing ones.  That would leave a gap in our sequence.  However, I am deciding now that when the user deletes an entry that the delete will also renumber file numbers (rename them) so that there will be no missing values in the file number sequence.  

What Does That Assumption Mean To Us Now?

That means we can simply get the list of files (which we already have) get the max N value and increment it by one for our new file number to be used in the file name.

 

With all of that in mind, here’s the code we are going to use to increment the file number.  We need to place this code in the else statement of our GenerateFileName() method.

Increment File Number

C#
List<short> fileNumbers = new List<short>();

foreach (string f in allCurrentFiles)

{

    String fileName = System.IO.Path.GetFileNameWithoutExtension(f);

    fileNumbers.Add(Convert.ToInt16(fileName.Substring(fileName.Length - 3, 3)));

}

int maxValue = fileNumbers.Max<short>();

maxValue++;

FileName = String.Format("{0}-{1}.rtf", YMDDate, maxValue.ToString("000"));

 

Let’s examine this line by line.

Image 17
 

Line 49 creates a new local List<short> (List of short types) which we will use to store our file numbers.

What Is A Short?

A short type is an alias keyword in C# which is another way to define a type which is an Int16.  That means it is a two-byte integer (16 bits).  Short is a convention that has long been used to indicate that a integer type is smaller than the basic integer type.  There is also a Long type which is a 64 bit (8 byte) integer size. Of course the max value of a short is smaller because it it only has 16 bits to store a value.  Since the short is a signed integer (can contain values both positive and negative) it can hold a max value of 32767.

That means we are limiting the number of files to a particular Y-M directory to 32767.  That should be fine, especially if you consider that Windows and File Explorer may even struggle with that many files in a directory.

On line 50 you see that we are using the C# foreach construct for iterating through a list.  All we have to do is provide a type (a string in this case) and a temporary variable (f) where each value in the list will be stored for our use, each time through the loop.  

System.IO.Path

On line 52 we use that value as a parameter to the GetFileNameWithoutExtension() method.

That method is another library method found in the System.IO.Path library which .NET provides for us.  There are many helpful methods related to working with paths, files, directories, etc in there.

Summary of This Code

In our case, we just want the filename and no extension because what we are really trying to do is get the current file number value of each file and add it to a List<>.  We are adding it to a List<> because the List<> type offers a method called Max<>() which will determine the max value it contains and return it to us.  That’s going to make our code very easy.

I’m getting ahead of our code, but that’s a good summary of what we are doing.

 

After line 52 runs, fileName will be filled with one of our file names (minus its extension) that has already been created.

So fileName might look something like: 2017-12-06-001

Line 53 actually contains 3 code statements (or operations) and again order of operations comes into play.  Of course, the right-most method call is run first because it is in the innermost set of parentheses.  In this case the line does the three following things in order:

  1. Calls String library’s Substring on the fileName variable to get a specific part of the string

  2. Calls the library method Convert.ToInt16() to convert the string value we pass to it to a valid Int16 (short value)

  3. Finally, we add the converted value to our List of short values.

Substring

When we call Substring on our fileName, it starts at the index value we provide in the first parameter.  In this case we provide the value, fileName.Length - 3. fileName.Length returns the Length of the string as a count of characters.  In our case the returned value will be 14 since our filenames will always be in the format of (2017-12-06-001).

Here’s an example of what fileName looks like as an indexed array of characters.

Image 18

However, notice that I was limited to using only one character to represent 10,11,12 and 13.

The point here is that the max index is always one less than the actual length of the string.

That’s because C# uses zero-based indices in arrays (ie arrays start counting at zero).

Now, if we look closely, we can see more clearly what fileName.Length - 3 means.

The length is 14 and when we subtract 3 from that we get 11, so we see that the Index that the Substring method will start with (the value of the first parameter) will be 11.  Now, take a look at the indices and you’ll see that the second 1 is the 11th index and it is over the second to last zero.  That’s where we want to start getting our substring value.

Substring : Second Parameter

The second parameter supplied to the Substring method is the number of characters that we want to retrieve and we have given it the value of 3.  So starting at index 11 and counting over three (starting with counting the character you are on) we get the last three characters in the string.

In our case the value returned from Substring will be “001”.  It is a string value at this point and we need to convert it to an Integer.

That’s where the second method call comes into play.

Convert.ToInt16

We call the static Convert.ToInt16() provided to use by the .NET library and simply pass in a valid string.  By valid string it means that it contains characters that can be reasonably converted to numeric values.  

Ignores Leading Zeros

The nice thing about Convert is that it will simply ignore the leading zeros and convert the value.

The value returned in this example case is an integer 1.

Finally, we call our List method Add() to add that value to our list.  This is all wrapped in our foreach loop so that we add every file number from every file that currently exists in our allCurrentFiles array.  

All To Get The Max File Number

We did all that work simply so we can get the current Max file number value easily.

On line 55 we create a new local integer variable to store the value and call the List provided method called Max().  That method simply returns the Max value that the list contains.  When it does, we store it in a local variable for our use.

On line 56 we simply increment the value we have, since we are creating a new file name and along with it a new file number.

Finally, on line 57 we set our FileName member to the value returned by our String.Format call which is used to insure our FileName format is always correct.  The difference between this String.Format() call and the one in the if statement is that we now reference our maxValue instead of 1 to insure it is the new valid current value which should be used for the file number.

It Works!  Mostly

This code will now build and run and save a file in the proper directory.

However, there is one more thing to fix back in our MainPage.xaml.cs.  It’s a very small thing but if you are going to try out the new functionality, I think we should fix it so the app works more like we expect it to.

Fix MainRichEdit To Clear

Here’s the problem that occurs right now.

If you run the code and type text in Entry1 and then move to any other day, you will find that the Entry1 still displays the text you typed in the RichEditBox even though when you switch away to a new day’s entry it should clear it.

The code we need to fix is over in MainPage.xaml.cs in the MainCalendar_SelectedDatesChanged() method.

Inside the while loop I clear out the extra PivotItems (Entries) but I forgot to clear out the RichEditBox Document.

To fix the problem, we just need to add the following line to the method:

C#
MainRichEdit.Document.SetText(TextSetOptions.ApplyRtfDocumentDefaults, "");

Image 19

 

That simply sets the MainRichEditBox Document property (object) text to be empty.

 

Get Final Chapter Code

Get the DailyJournal_v019.zip and build it and try it out.  It'll save multiple entries

Summary

Now, the code will save all of your files into the proper Y-M directory and everything related to saving a document works quite well.

Loading Existing Entries

However, when you move back to a date which already has entries created, they will not be loaded again so it effectively look as if you never saved any entries.  The only way to examine entries right now is to navigate down to the folder using File Explorer and open the documents using another app like WordPad.  

Next Time

This is the initial problem we set out to fix in this chapter but we had quite a bit of other work to do.  Next time we will fix the LoadEntriesByDate() method and take care of this issue and we’ll be very close to a fully working basic app.

History

2017-12-05 : First publication

License

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