Tuesday, December 08, 2009

Forgive Them...For They Know Not What They Do...With Your App

If you've been following along with the home game, then you are aware that there are many excellent UI patterns that you can use to improve the user experience you deliver to your users in your applications.  Today, I wanted to explore another simple yet powerful pattern: "Forgiving Formats".  This pattern is perfect for those applications in which speed to data entry is key (or at least desirable in most circumstances).  I know in many of the business applications I have encountered over the years, we tend to develop nice, complex forms to capture all of the data the customer says they absolutely need.  When you come back and look at the database later, however, you often find that only a few key fields are filled out (just the required ones if you're using validation!).  The Forgiving Format pattern is all about letting users enter those key bits of data quickly, without having to worry overly much about the way they are entering the content.  This pattern puts the onus on you as the developer to do the heavy lifting, not the end user.  Of course, this is one of the tenants of interface first design.  It allows you to simplify the UI and make the problem really about the behind-the-scenes code.

A great example of the Forgiving Format pattern is Google Maps.  When searching for an address, you can use a variety of inputs.  For example, when searching for a location in my town, I can put in:

Aurora, OH
Aurora, Ohio

This flexibility allows me to focus on the job at hand, not on the exact way I have to format the address so that Google understands it.  This lets me get my work done faster, so obviously it makes me a happy user.

You can apply this same idea to your applications.  I find it can be useful both for allowing quick data entry for Notes documents as well as for use as a search mechanism.  The underlying idea is simple.  As the developer, you have to account for the various input types the user may enter and then code for those instances.  In my Movie Review sample database, the main input is obviously movie information.  While the form allows you to capture more data, there are only a few fields that are key to building a good database of movie knowledge.  These include the movie title, the rating, the genre and the running time.  I created a Quick Movie Entry option in the database to show this in action.  First, let's take a look at how I implemented this.

I created a caption in the frame so that the user could free up screen real estate if they didn't want to use this option.  It gets it out of the way if the user will be doing something other than data entry.  However, if they want to quickly add a bunch of movies, they can expand the caption area and then will see an input line for the movie information.  Of course you could break the input into a bunch of separate fields to capture the user input, but a single field is easier and more elegant. 

I included a brief explanation of what the user should enter with this control. This particular pattern works best when you have information that is targeted to a specific objective and when the input is something that you can prepare for. Leaving it completely open ended would be incredibly hard to program for and would likely result in failure. In this case, I'm expecting those four particular bits of data and I want the user to enter them in that order, as specified by the help text. When you choose to implement this functionality in your own application, you will have to weigh how flexible you want to be with the input field and possible combinations of entries vs. the complexity of the evaluating code.

In the Movie Review example, the user can enter the title, rating, genre and running time and then hit the "Go" button. If the code successfully recognizes the entry, a new document is created in the database and the user is informed via a popup message (using the fade in/fade out technique). If the input can't be recognized, the user sees an error message.

As far as the backend code goes, it can be fairly simple or very complex, depending again on how flexible you want to make this functionality for the end user. In this example, there are two fields that I'm actually mapping to already established categories. The rating should obviously be a valid entry (G, PG, PG-13, R and NC-17) and the genre has values such as Science Fiction, Drama, Comedy, etc. To evaluate these, I used a Select Case statement. This allows for the "forgiving" part of this pattern.

In the case of the rating, one user might enter a NC-17 movie as NC17 while another might use X and another might use XXX. They could also use varying values of upper and lower case (e.g. Nc17, nc-17, xxx). For each of these different possibilities, a Select Case statement makes it nice and easy to evaluate the input. I did the same thing with the genres. For a Disney movie, users might enter options like cartoon, animated, animation, etc.

Below is the sample code behind the button. Feel free to use this as a guide, but remember there is no robust error handling in the example.

Sub Click(Source As Button)

Dim workspace As New NotesUIWorkspace
Dim session As New NotesSession
Dim db As NotesDatabase
Dim uidoc As NotesUIDocument
Dim movieDoc As NotesDocument
Dim entryDoc As NotesDocument
Dim movieEntry As Variant

Set db = session.CurrentDatabase
Set uidoc = workspace.CurrentDocument
Set entryDoc = uidoc.Document

On Error Goto ShowError

movieEntry = entryDoc.MovieEntry

'Here's where you are going to do the meat of your work.
'You need to determine just how much logic you want to put
'in place to decode what the user has entered. I've given a
'couple of simple examples below.

Set movieDoc = db.CreateDocument
movieDoc.Form = "Movie Review"

'Let's deal with the title first
Dim movieTitle As String
movieTitle = Strconv(movieEntry(0), 3)

Select Case movieEntry(1)
Case "G", "g":
movieRated = "G"
Case "PG", "pg":
movieRated = "PG"
Case "PG13", "PG-13", "pg13", "pg-13":
movieRated = "PG13"
Case "R", "r":
movieRated = "R"
Case "NC17", "NC-17", "X", "XXX", "nc17", "nc-17", "x", "xxx":
movieRated = "NC17"
Case Else
movieRated = ""
End Select

Select Case movieEntry(2)
Case "Science Fiction", "scifi", "sci-fi", "SciFi", "Sci-Fi", "Sci-fi", "Scifi":
Category = "Science Fiction"
Case "Action", "action", "Adventure", "adventure":
Category = "Action"
Case "Comedy", "comedy", "funny", "Funny", "humor", "Humor":
Category = "Comedy"
Case "cartoon", "cartoon", "animation", "Animation", "animated", "Animated":
Category = "Animation"
'etc, etc....
Case Else
Category = ""
End Select

'Here you'd actually perform the task (such as creating the
'new document if everything validated correctly. It should
'if you are careful with your forgiving format.
If movieTitle <> "" And movieRated <> "" And Category <> "" Then
Dim newMovie As NotesDocument
Set newMovie = db.CreateDocument
newMovie.Form = "Movie Review"
newMovie.MovieTitle = movieTitle
newMovie.Category = Category
newMovie.MovieRated = movieRated
newMovie.MovieTime = movieEntry(3)
Call newMovie.Save(True, False)

'Display cool fade-in success message here
'Display cool fade-in error message here
End If
Exit Sub

End Sub

This LotusScript was converted to HTML using the ls2html routine,
provided by Julian Robichaux at nsftools.com.

When you break it down, I think that this a very simple UI pattern to implement in your applications. As always, it comes down to trying to simplify the user's task. While this pattern is not applicable to all situations, I can think of a good number of Notes applications I use frequently that could benefit from the Forgiving Format pattern. Does one of yours? Give it a try and let me know how it goes.

Labels: , , ,

permalink | leave a comment

Friday, July 31, 2009

Add Document Ratings To Your Notes Client Designs

Way back when I was actively developing stuff on a daily basis, I wrote about a quickly cobbled together idea I had to include a document rating system in a Notes client application (might want to read that first). Document Rating is a familiar "Web 2.0" design pattern that can actually be quite useful. Document ratings allow users to democratically decide on good content and by using this technique the best stuff will bubble up to the top (at least theoretically). In the original post, I wanted to explore the concept of using editable view columns with icons in a little more detail while showing off some cool functionality in the client. With the frequent use of this design pattern in modern websites, I think it is safe to assume that it is a pattern that is around to stay. Thus, I wanted to make the solution a little more robust. As with most of the things I put here on Interface Matters, I've removed a lot of the extraneous code that you really should use in production so that you can focus on the core of the technique, but the sample database below should be enough to get you started.

Before we begin going through the technique, I suggest you read through the Rate Content design pattern page on UI Patterns.com. That page should give you a little more detail on what I am shooting for. For our purposes, let's focus on the following mechanisms:

* Voting mechanism.
* Display the average rating an item has received.
* Display explanatory comments from users rating an item.

I've left off the three mechanisms below, but they seem like a perfect fit for dashboard-like functionality.

* Show the highest rated items.
* Favor quality items.
* Related items.

In the original post, I wasn't taking into account security on the document that is being rated. Thus, this technique really wouldn't work for most real world situations. To overcome this, I decided to use the concept of stub documents to capture each user's rating of a particular document. This offers several advantages. First, I can easily perform a lookup to see if the user has already rated a given document or not. Second, it allows me to extend the original functionality by allowing for additional information such as comments. Finally, this idea respects the security of the main document, as the average rating is calculated by an agent so that the user never has to have edit access to it. Thus, the flow of the process goes something like this:

1. User clicks a star to define their rating.

2. If desired, pop up a dialog box to capture their comments about why they rated the document as they did.

3. Write a stub document that captures a unique key value that corresponds to main document and user, their rating and comments.

4. Run an agent (that executes with proper authority) to update the main document with the new average rating. This agent will loop through all of its rating documents to determine the average.

At the risk of making this post too long, I am going to break this down step-by-step:

Step 1: Add columns to your view. I created a separate column for each star, since I want to know explicitly which one was clicked. Set the column to be editable and set it to "Display values as icons".

Step 2: The column value checks the current average rating of the document and based on the position of the column determines if it should show the filled or unfilled star.

@If(num_Rating >= 1; "star_red"; "star_open") and so on...

Step 3: Add the code to the InViewEdit event. I've included a bunch of comments to the code so you can follow what is happening. It's here that we first check to see if the user has rated the document previously and decide to allow them to edit that rating or not. I chose to implement this as a flag that you can enable or disable in the code itself for easy demonstration, but you could make this and some of the other options part of the db configuration. If the user hasn't voted yet, the InViewEdit code creates the stub doc, asks them for a comment (if appropriate) and kicks off the update agent.

(Please note I added some line breaks into the code samples. If you want to copy and paste, do so from the sample database)

Sub Inviewedit(Source As Notesuiview, Requesttype As Integer, Colprogname As Variant,
Columnvalue As Variant, Continue As Variant)

Dim workspace As New NotesUIWorkspace
Dim session As New NotesSession
Dim db As NotesDatabase
Dim doc As NotesDocument
Dim caret As String
Dim flag As Boolean

'Set AllowChanges to 0 to keep users from changing their rating. If set to 1, then can modify their initial value
Const AllowChanges = 1

'Get the CaretNoteID - exit if it does not point at a document
caret = Source.CaretNoteID
If caret = "0" Then Exit Sub

'Get the current database and document
Set db = Source.View.Parent
Set doc = db.GetDocumentByID(caret)

Dim ratingsView As NotesView
Dim ratingsDoc As NotesDocument
Dim DocUNID As String
Dim key As String

'We need to check to make sure the user hasn't already rated this document
Set ratingsView = db.GetView("viewRatingsByKey")
DocUNID = doc.UniversalID
key = DocUNID & "_" & session.CommonUserName
Set ratingsDoc = ratingsView.GetDocumentByKey(key)

If Not (ratingsDoc Is Nothing) Then
If AllowChanges = 1 Then
flag = workspace.DialogBox("dlgRatingsCommentDialog", True, True, True, True, False, False,
"Enter A Comment About Your Rating"
, ratingsDoc, True, True, False)
If flag = True Then
Call ratingsDoc.Save(True, False)
Call RunTheAgent(db, doc) 'Call the agent to update the main document with the new rating value
End If
Messagebox "Sorry...you've already rated this document"
End If
Exit Sub
End If

'If we got this far, the user hasn't voted yet, so we'll take their entry. Here we are creating the new stub document.
'This document uses a combination of the main document's UNID and the user name for the key value. This
'stub document could capture as much information as you need...just add the appropriate fields. If you don't
'ever plan to show the underlying content to the end users, you don't need an actual backend form.
Dim newRatingsDoc As NotesDocument
Dim item As NotesItem
Dim readersItem As NotesItem
Dim newValues( 1 To 2 ) As String
newValues( 1 ) = session.UserName
newValues( 2 ) = "[Admin]"

Set newRatingsDoc = db.CreateDocument
newRatingsDoc.Form = "frmRatingsDoc"
newRatingsDoc.txt_TargetUNID = DocUNID
newRatingsDoc.txt_RatingComment = ""
newRatingsDoc.txt_Key = DocUNID & "_" & session.CommonUserName
Set item = New NotesItem(newRatingsDoc, "nam_UserRated", session.UserName, NAMES)

'To keep ratings private, uncomment the line below. But if you do this, probably no use asking for comments.
'Set readersitem = New NotesItem(newRatingsDoc, "read_Users", newValues, READERS)

Select Case Colprogname(0)
Case "Star1"
newRatingsDoc.num_Rating = 1
Case "Star2"
newRatingsDoc.num_Rating = 2
Case "Star3"
newRatingsDoc.num_Rating = 3
Case "Star4"
newRatingsDoc.num_Rating = 4
Case "Star5"
newRatingsDoc.num_Rating = 5
End Select

'Modify the code for comments as necessary to suit your needs

'Now lets see if the user wants to add a comment
Call ratingsView.Refresh

'Option 1 - Use this if you want to pull up the comment dialog but don't require a comment.
'Call workspace.DialogBox("dlgRatingsCommentDialog", True, True, True, False, False, False,
"Enter A Comment About Your Rating", newRatingsDoc, True, True, False)

'Call newRatingsDoc.Save(True, False)

'Option 2 - Use this if you want to require comments before a rating can be saved.
flag = workspace.DialogBox("dlgRatingsCommentDialog", True, True, True, False, False, False,
"Enter A Comment About Your Rating"
, newRatingsDoc, True, True, False)
If flag = True Then
Call newRatingsDoc.Save(True, False)
End If

'Call the agent to update the main document with the new rating value
Call RunTheAgent(db, doc)

End Sub

Sub RunTheAgent(db As NotesDatabase, doc As NotesDocument)

'Call the agent to update the main document with the new rating value
Dim agent As NotesAgent

Set agent = db.GetAgent("agtCalculateRatings")
Call agent.RunOnServer(doc.NoteID)

End Sub

This LotusScript was converted to HTML using the ls2html routine,
provided by Julian Robichaux at nsftools.com.

Step 4: Add the agent that updates the main document with the average rating. This agent is called by the InViewEdit code as described above. This agent will take the main document, find all of the stub rating documents based on the UNID of the main doc and then loop through the collection, simply adding the values of the ratings and then dividing by the number of ratings to get the average. This average value is then written back to the main document. Make sure that this agent is set to run with an id that has access to all of the main documents, not as the current user.

Sub Initialize

Dim session As New NotesSession
Dim db As NotesDatabase
Dim agent As NotesAgent
Dim ratingsView As NotesView
Dim mainDoc As NotesDocument
Dim ratingsDoc As NotesDocument
Dim ratingsCollection As NotesDocumentCollection

Set db = session.CurrentDatabase
Set agent = session.CurrentAgent
Set ratingsView = db.GetView("viewRatingsByUNID")
Set mainDoc = db.GetDocumentByID(agent.ParameterDocID)
Set ratingsCollection = ratingsView.GetAllDocumentsByKey(mainDoc.UniversalID)

If (ratingsCollection.Count = 0) Then
'Whoops...something went wrong and you'd do some good error trapping here
Exit Sub
End If

Dim ratingsCounter As Long
Dim numberOfRatings As Long
Dim averageRating As Long

ratingsCounter = 0
numberOfRatings = ratingsCollection.Count
Set ratingsDoc = ratingsCollection.GetFirstDocument

'Let's add up the total of all ratings first
Do While Not (ratingsDoc Is Nothing)
ratingsCounter = ratingsCounter + ratingsDoc.num_Rating(0)
Set ratingsDoc = ratingsCollection.GetNextDocument(ratingsDoc)

'Now, we average the ratings by taking the total and dividing by the number of votes
(assuming all have the same weight)

averageRating = ratingsCounter / numberOfRatings

'And now we can set this on the main document and be on our way
mainDoc.num_Rating = averageRating
Call mainDoc.Save(True, False)

End Sub

This LotusScript was converted to HTML using the ls2html routine,
provided by Julian Robichaux at nsftools.com.

There are a lot of ways in which you could flush this technique out even more. For example, I added an embedded view to the main document form that is used to show the stub document ratings. This way, when you open a particular content document, you can see in detail who rated the content and why (via their comments). This is a nice way to provide users with real meaning behind the values. I'd be interested in hearing about other ways you might use this functionality as well.

The sample database is available below. You've likely seen this before, as it contains a variety of sample design patterns, some of which I haven't talked about yet. You can switch to the "User Ratings" view by using the selection box in the lower right corner of the main screen. Yes...this database is a bit ugly. I was playing with some different theme ideas and didn't want to make another boring looking Notes app. ;-)

Have fun, let me know what you think and please share with the others if you find a use for this in your company's applications.

Labels: , , , ,

permalink | leave a comment