Keep the Customer Satisfied (Matt Gertz)

As I’ve written elsewhere, the toughest critics for any work you do can always be found in your own family.  Pleased at the immense work that I’d done in scanning in and tagging all of my photos and media (as noted in this post), and with my ego sufficiently boosted by writing a screensaver in VB that would display not only the pictures but the tags associated with them, it was a deflating experience to have my middle child, Aidan, pepper me with comments like “That’s not me; that’s Brandon” or “You didn’t include Grandpa in the tags, but I can see his ear in this photo.”

In order to preserve the peace (and find the inevitable set of photos with bad tagging or poor rotation), I needed to open up the code to give the kids a way to let me know which photos needed to be re-examined.  But how to do this?  I’ve currently got the photos set to display for 30 seconds, which is a limited amount of time for the kids to get over to my desk, pull out the keyboard tray, and somehow record a flaw without turning off the screensaver.

“X” marks the spot

So, I considered my customers.  My kids would likely be coming over from the couch, which is to the left of my desk.  Pulling out the keyboard tray could jiggle the mouse and end the screensaver, so I wanted a key that they could reach without moving anything.  Furthermore, it needed to be a memorable key, and so I settled on “X” (as in “X” marks the spot), which is of course located on the left side of the keyboard.

Now, this sounds a lot like overthinking, doesn’t it?  But really, all design decisions need to take in account customer abilities and cognitive associations.  I could just as easily have chosen “F5” as the keystroke, but besides creating an added potential for moving the mouse disruptively (an accessibility issue), the fact is that the key “F5” would have no association for my kids whatsoever.  “X,” however, is an “interesting” letter and likely to stick in their minds.

So, anyhow, here’s how I went about it code-wise:  I opened up the project that’s presented in the aforementioned post and navigated to the code for the frmScreenSaver object.  Inside that code, I added an event handler for “X” thusly:

    Private Sub frmScreenSaver_KeyPress(ByVal sender As Object, _

ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles Me.KeyPress

 

First thing to do is check to see if “X” was pressed:

 

        If e.KeyChar = “x” OrElse e.KeyChar = “X” Then

 

OK, “X” was pressed.  Now, do I actually have a file that I’m currently showing?  If my screensaver couldn’t find any files, then I wouldn’t have one.  I can leverage the m_currentFile variable to check:

            If Not String.IsNullOrEmpty(m_currentFile) Then

 

OK, I’ve got a file.  So, I’m going to log this filename to a text file, appending it to whatever is already in that file (or creating it if none have been logged yet).  (Every Saturday, I check this file for the accumulated complaints, examine the photos, make the required fixes, and delete the log file.)

                My.Computer.FileSystem.WriteAllText( _

My.Computer.FileSystem.SpecialDirectories.MyDocuments _

& “FilesToFix.txt”, m_currentFile & vbCrLf, True)

 

Pretty simple!  But I need to provide feedback to my kids that their keystroke was accepted, and so I add a simple “beep:”

 

                My.Computer.Audio.PlaySystemSound(Media.SystemSounds.Exclamation)

            End If

    End Sub

 

And that’s it.  I build the project in retail, rename it to be a *.scr file, and copy it up the the system directory. 

“Is that really necessary to look at, dear?”

The second complaint I received about the screensaver came not from my kids but from my wife, the crux of the problem being that the screensaver didn’t discriminate between good pictures and bad pictures.  This could be somewhat embarrassing when friends were visiting.  For example, I’ve got a set of photos from when I was in Dubai on a recruiting trip a few years back.  The photos weren’t just taken by me, but by my co-workers as well.  Evidently, one of them was highly impressed with the bidet in their hotel bathroom and decided to commemorate it with a photo — not something that my wife was crazy about seeing on the screen.  And, while I am often impressed by the artistic abilities of my kids, the fact of the matter is that their photos are often punctuated by close-ups of the dog’s butt, their best friend’s eyeball, or some incredibly blurry thing that can’t be made out at all.

So, to address this problem, I decided to add a “nuke” feature to the screensaver.  When the “N” key (another easy-to-reach key with a good mnemonic — “N” stands for “nuke”) is pressed for a given photo in the collection, I want to eliminate it and move to the next picture.  Sounds simple, no?  It’s just a modification of the “X” code from above.

But here’s the problem:  my kids are wonderfully smart, but I don’t trust them to be good judges of whether or not a photo should be permanently deleted (and, depending on the timing, they might use the key just as the photo changed, thus deleting the wrong photo).  I therefore needed to construct a way to send the photo to a “penalty box” until I had the time to review the photo.  Furthermore, this “penalty box” needed to be able to persist between sessions.

Here’s how I set about it:  first, in the same event I created above, I check for the “N” key and for the existence of the file:

        ElseIf e.KeyChar = “n” OrElse e.KeyChar = “N” Then

            If Not String.IsNullOrEmpty(m_currentFile) Then

        

I log the picture just as I did before, although to a different file this time:

 

       My.Computer.FileSystem.WriteAllText( _

My.Computer.FileSystem.SpecialDirectories.MyDocuments _

   & “FilesToNuke.txt”, m_currentFile & vbCrLf, True)

                My.Computer.Audio.PlaySystemSound(Media.SystemSounds.Exclamation)

 

I then remove the photo from the list of files by finding its index and calling “RemoveAt()”:

 

                Dim index As Integer = m_files.IndexOf(m_currentFile)

                If index >= 0 Then

                    m_files.RemoveAt(index)

                    m_fileCount -= 1

                End If

 

And then I call “DrawPicture(),” which forces a move to the next picture without waiting for the timer:

 

                DrawPicture()

            End If

        End If

 

Unfortunately, if the screensaver is interrupted before I get a chance to review the “photos to nuke” on Saturday, it will re-create the file list with the bad photo(s) still in it the next time that it restarts.  Therefore, I need to adjust how I create the file list in the frmScreenSaver_Load() event handler – it needs to read from the “FilesToNuke.txt” file (if it exists) and remove such photos from the photo collection that it generates. 

 

Here’s how I do that – first, I change m_file’s type to be a normal collection rather than a read-only collection, so that I can alter it:

 

    Private m_files As New Collection(Of String) ‘ List of files

 

Then, in the Load event handler, I make a few minor modifications (in bold):

 

        Dim files As ReadOnlyCollection(Of String) ‘ List of files

        If m_Options.UseSubdirectories = True Then

            files = My.Computer.FileSystem.GetFiles(m_Options.Directory, _

              FileIO.SearchOption.SearchAllSubDirectories, “*.jpg”)

        Else

            files = My.Computer.FileSystem.GetFiles(m_Options.Directory, _

              FileIO.SearchOption.SearchTopLevelOnly, “*.jpg”)

        End If

 

That code is the same as before, except that I read in the collection for the new variable “files” rather than m_files. 

 

Now, we encounter a pet peeve of mine – the method GetFiles returns a read-only collection rather than just a normal collection.  This means that we can’t remove anything from the collection we get back from GetFiles(), and as noted above, I need to remove the questionable files from the collection until such time as I get around to reviewing and deleting the files permanently.  (One day, I’m going to write a blog which discussed the correct reasons for using read-only objects – I’m all for security, but it’s too easy go overboard with it sometimes, in my opinion.) 

 

I have two choices – either write my own (thorough!) routine for enumerating files, or else bite the bullet and copy the read-only collection to a modifiable collection.  (Of course, I could alter the Timer handler so that I check the random result against “bad” filenames every time the timer fires, but that would be taking on a perf hit on every event, not just the initialization.)  Being inherently lazy, I’ll just copy the collection (ooh, I feel so dirty!) into m_files, which is the version that I ultimately cache & use internally:

 

        ‘ Copy to a writeable collection (why is the other collection read-only?)

        For Each f As String In files

            m_files.Add(f)

        Next

 

The worst (ethically) is over; I will now load in the set of questionable files from the “FilesToNuke.txt” file and remove them from the collection.  Fortunately, the files being separated in the file by a carriage return, it’s easy for me to read them in and separate them into a “nuke” list, which I can then iterate to remove the corresponding items from the main collection.  (I could also have read this list a priori and prevented the items from being copied into the modifiable collection above, but that’s actually less performant, as I would have far more misses than hits, even if I removed items from the “nuke” list after the corresponding entry was found.)