Keep the Customer Satisfied (Matt Gertz)

Keep the Customer Satisfied (Matt Gertz)

  • Comments 4

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.)

 

        ' Load files to ignore

        Try

            Dim separator() As String = {vbCrLf}

 

            Dim sFilesToIgnore As String = _

              My.Computer.FileSystem.ReadAllText( _

My.Computer.FileSystem.SpecialDirectories.MyDocuments _

& "\FilesToNuke.txt")

 

            Dim sarrFilesToIgnore() As String = _

sFilesToIgnore.Split(separator, StringSplitOptions.RemoveEmptyEntries)

 

            For Each s As String In sarrFilesToIgnore

                Dim index As Integer = m_files.IndexOf(s)

                If index >= 0 Then

                    m_files.RemoveAt(index) ' Don't want to see this file anymore

                End If

            Next

        Catch ex As Exception ' File not found

        End Try

 

Done -- build, rename, copy to the system directory.  Now, my wife and kids and can easily log files that *might* need to be deleted and, furthermore, be spared the sight of them, no matter how many times the screensaver stops and starts, until I get a chance to make the permanent deletion.

 

That’s it; all done (until my family requires me to add more functionalityJ).   I’ve uploaded the new code to my “Temple of VB” site (see the link below -- I'm going to stop uploading files to this blog since it's problematic to maintain the code in two sites). 

 

‘Til next time,

  --Matt--*

Attachment: http://code.msdn.microsoft.com/Project/Download/FileDownload.aspx?ProjectName=templeofvb&DownloadId=4565
Leave a Comment
  • Please add 5 and 4 and type the answer here:
  • Post
  • Did you add the Tags to the EXIF data in the jpeg photos?

    The .NET BCL is very poor at getting metadata from photo and media files.   Can this get improved in future .net versions with:

     - open image file (jpeg, tiff, png, bmp), get dimensions, pixels per centimeter, color depth, number of colors/color profile (CMYK, gray, RGB), etc

     - open audio file (wave, mp3), get duration, bit rate, sample size, number of channels, tags (title, album, etc)

     - oepn video file (avi, asf, wmv, mpeg 1, mpeg2, mpeg 4), get duration, frames per second, frame size, audio format, audio sample size, number of audio channels, etc.

    This would be nice to get the .NET BCL up to speed and somewhere comparable to the Java Media Framework in terms of metadata handling.

  • Hi, Greg! Yep, they are in EXIF data.  

    I agree with you on the difficulty in getting metadata back from photos -- it really should be as simple as a set of properties (or, really, a collection of properties to allow for expansion).  My biggest gripe is that it is nigh-impossible to remove a tag from a photo once it's in there, without recopying the whole thing.  

    I suspect that the reason that the support for it is so light right now is that other metadata standards were in flux AND the .NET team was necessarily mostly focused on other issues during the last version rev.  But I've got some friends over there and I'll ask them when I see them on Wednesday.

    --Matt--*

  • Great consumer-driven architecture there, Matt.

    And I agree with Greg that we need more first-class managed APIs for multimedia scenarios like this.

    I think in part the difficulty is in the lack of standardization across operating systems and file formats for a consistent meta-data protocol.

    Ideally (to me at least), metadata would be stored in an XML format in an alternate named FileStream. NTFS (and Mac OSes too) has supported this for years though most applications don't take advantage of it. But this problem is that HTTP and other network transmission protocols and programs (probably) don't understand the notion of multiple streams for a single file so when you try to copy a file, burn it to disk, email it, etc, the metadata is lost unless its embedded in the default unnamed stream :(

    Plus streaming media (like MP3s) this way has its problems because it's easier to encode certain data like Title and Artist at the beginning of the file so that this information can be displayed to the user and some data may be stored at the back so that the music can be found quicker.

    But what can be done, eh?

  • Hi, Anthony,

     Yeah, standardization is the bigger problem (sigh).  Not much to be done except either voting with your money or sitting back and watching who gets the lucrative deals (a la Blu-Ray, VHS, etc).

    I used to be a Mac programmer before I joined MS, back around systems 5 - 7, and I agree that the separate fork in the file was a great thing for storing resources and the like; I wished we leveraged this ability in NTFS more (although, as you point out, it has implications not just in protocols but also with downlevel systems like FAT).  

    Interestingly, I can see other areas where we'd use the fork -- you could imagine being able to persist state information about a VB code file (history, selection, etc), without worrying about losing it on a checkin.  

    --Matt--*

Page 1 of 1 (4 items)