Windows DevCenter    
 Published on Windows DevCenter (http://www.windowsdevcenter.com/)
 See this if you're having trouble printing code examples


Liberty on Beta 2

Creating an Application from Scratch, Part 2

by Jesse Liberty
01/31/2006
Use ClickOnce to Deploy Windows Applications

Back in November, I wrote a column in which I promised to create a project from scratch, while you watched. At the time I was calling the project Concord, but for no good reason I've renamed it PeopleLikeMe.

Well, time and clients and paying work intervened, and only this weekend have I actually started developing the project. Progress has been pretty good. Here's where things stand: I've created an ASP.NET 2.0 application with a page named UpdateRecords in which you enter your own AccountID from Amazon and create (or update) a record of all of your own reviews. (Soon to be implemented, it will also find the ID of everyone else who has reviewed the books you reviewed, and then find all the books they have reviewed.

The first cut of the database is fairly simple, consisting of only three related tables as shown in Figure 1.

As you can see, each book has an entry in the Books table and all the reviews for all of the books (by all of the reviewers) are stored in the Reviews table. Every reviewer has a ReviewerID (assigned by Amazon as the CustomerID) and that ReviewerID is tied to the user's security and personalization ID through the User_To_Reviewer table. It isn't clear that we need the Books table at all, as we may end up generating all the information we need dynamically through a query to the Amazon Web Service by ASIN (ASIN is Amazon's generic identifier for all items; for books this value equals the book's ISBN) but for now it is a convenience and not difficult to populate.

Figure 1
Figure 1. Our database starts out as just three related tables.

The Amazon Web Service

All of the data about user reviews is [1] gathered from the Amazon E-Commerce Web Service, which is infinitely safer and more convenient than screen-scraping (and eliminates the copyright infringement issue). To get started, you'll need an Amazon Web Services Account.

Begin by going to Amazon and clicking on their link for Web Services (Figure 2).

Figure 2
Figure 2. Link to Amazon Web Services

Follow the steps to obtain the necessary membership keys [2] by clicking on Create Your Free Amazon Web Services Account, and then navigate to the (free!) Amazon E-Commerce Service.

You may want to download the code samples which (as of this writing) contain an out-of-date sample .NET (1.x) control. You'll certainly want the documentation,which includes a PDF file that, once you realize it was written for non-.NET programmers, can help you figure out which objects you want to use and which properties to set. For example, to find all the reviews by a given user, you'll discover there is a CustomerFullResponseGroup and the documentation shows (in part) that the result of making a request for information about a specific customer returns the XML shown in Figure 3 (excerpt).

Figure 3
Figure 3. The XML returned by a REST request

I've put in arrows that indicate the tags that are translated into properties of the CustomerContentLookupResponse object which I then turned into C# code more or less like Example 1:


Customers[] customersFound = CustomerLookupResponse.Customers;
 if ( customersFound.Length > 0 )
 {
     Customer[] customerCollection = customersFound[0].Customer;
     CustomerReviews[] customerReviews =
       customerCollection[0].CustomerReviews;
     CustomerReviews custReviews = customerReviews[0];

    if ( pageNumber == 1 )
    {
       bool recordsCleared = ClearRecords( customerLookupRequest.CustomerId );
       if ( recordsCleared == false )
          break;
       totalPages = Convert.ToInt32( custReviews.TotalReviewPages );
       totalReviews = Convert.ToInt32( custReviews.TotalReviews );
    }

    Review[] reviews = custReviews.Review;
    foreach ( Review review in reviews )
    {
       InsertReviews(
          review.ASIN,
          customerLookupRequest.CustomerId,
          review.Rating,
          review.Summary,
          review.Content );
    }
    
Example 1 (Error checking elided)

Note: InsertReviews() is a method I wrote to insert this information into my database (not shown).

It turns out that to maintain maximum flexibility, many of these properties are collections, even when their property name is singular (and I hate that). For example, as shown in Example 1, the Customer property is actually a collection of Customer objects and the Review property is actually a collection of Review objects (why weren't these named Customers and Reviews?). You can also see that I make the (safe) assumption, given my query, that I will get back only zero or one customer, and that customer will have only zero or one set of reviews (though in the actual working code I do check these assumptions).

Programming ASP.NET

Related Reading

Programming ASP.NET
Building Web Applications and Services with ASP.NET 2.0
By Jesse Liberty, Dan Hurwitz

Setting Up the Request

The code shown above handles the response. The actual query requires creating three objects, provided by the Amazon proxy. I downloaded (as part of their code sample) the file AWSECommerceService.cs which I placed in my App_Code directory as shown in Figure 4.

Figure 4
Figure 4. The downloaded AWSECommerceService.cs file

This gives me full access to their objects. To set up the query, I create the following member variables:

 
private AWSECommerceService amazonService = new AWSECommerceService();

private CustomerContentLookup customerLookup = new CustomerContentLookup();
private CustomerContentLookupRequest customerLookupRequest =
   new CustomerContentLookupRequest();
private CustomerContentLookupResponse customerLookupResponse;

private string subscriptionID = "XXXXXXXXX";
private string associateTag = "XXXXXXX";
 
 Example 2

The LookupRequest object has a ResponseGroup property which is an array of strings that specify what type of response you want; in our case, based on the documentation we want CustomerFull. The code to make the inquiry is shown in Example 3:

 
customerLookup.AssociateTag = this.associateTag;
customerLookup.SubscriptionId = this.subscriptionID;
customerLookupRequest.CustomerId = searchCustomerTextBox.Text;
myReviewerID = customerLookupRequest.CustomerId;
int pageNumber = 1, totalPages = 1, totalReviews = 0;
lblMessage.Text = "Searching...";

while ( pageNumber <= totalPages )
{
   customerLookupRequest.ReviewPage = pageNumber.ToString();
   customerLookupRequest.ResponseGroup = new string[] { "CustomerFull" };
   customerLookup.Request =
      new CustomerContentLookupRequest[] { customerLookupRequest };
   try
   {
      customerLookupResponse =
         amazonService.CustomerContentLookup( customerLookup );
   }
   catch ( Exception ex )
   {
      lblMessage.Text = ex.Message;
      return;
   }
 
 Example 3

The response object has a property Customers which is an array whose first member is itself an array (misleadingly called Customer). That array should have one member with a property CustomerReviews (again an array, this time of one CustomerReviews object) which has two important properties: TotalReviewPages and TotalReviews. The TotalReviewPages property is used to set the totalPages variable so that we can request all of the pages (you receive up to ten reviews per page, and up to ten pages). I haven't yet figured out what you do if the reviewer has more than 100 reviews.

This code depends on having a text box (named searchCustomerTextBox) which is filled with the user's Amazon ID. So far, all I've implemented is a test that gets all my own reviews, I haven't yet gone on to the next step of getting all CustomerIDs for all the customers who reviewed the books I reviewed, and then getting all their reviews, so of course this code will change very soon to allow a CustomerID to be passed in as a parameter.

Running the Program

To run this program I need my CustomerID. This is easily obtained by going to any book I've reviewed and clicking on my name. This brings me to my profile page, and in the URL is my CustomerID as shown in Figure 5.

Figure 5
Figure 5. Your Amazon CustomerID appears in the URL of your Profile page.

With my CustomerID in hand, I can fire up my sketchy test page and to get all my reviews into the database, as shown in Figure 6.

Figure 6
Figure 6. Our ugly little test page--it works.

As you can see, this is an ugly little test page that takes a CustomerID (in this case mine) and fires off the code shown above. It then reports on which records were inserted into the database (the first set of numbers is the ISBN, the second is the user's ID) and also reports on any rejected attempts. For example, the ISBN 0596003218 was rejected because of the Primary Key constraint. This is appropriate, because it turns out I have two reviews of that book, and the database is designed to ensure only one review per book per user.

Because reading the rejected records is important, I've implemented event handling for selecting a listing in the bottom listbox, and I turn on AutoPostback programmatically after all the records have been added. The implementation for the SelectedIndexChanged method is incredibly simple, it just writes the entire selected text to lblMessage, as shown in Figure 7.

Figure 7
Figure 7. Simple event handling for rejected records

Checking the Database

All of this information captured from Amazon is stored in the database table Reviews, as shown in excerpt in Figure 8.

Figure 8
Figure 8. Captured data stored in the Reviews table

As you can see, the ASIN, ReviewerID, and Rating are captured, along with the Summary and Full Review. The final two columns may not be needed any time soon, but it is painless to include them so I do. It would appear that the ReviewerID is not normalized, but in fact, once we run this for many different reviewers, we'll need the ReviewerID column to associate the rating given for any one book by the various reviewers and thus to match those other reviewers to the current user's ratings.

Since the user may write new reviews or edit existing reviews, rather than fussing with updating, I just delete all the records and re-insert them each time the program is run!

 
 // in btnFindUser_Click event handler...
 bool recordsCleared = 
    ClearRecords( customerLookupRequest.CustomerId ); 
 
private bool ClearRecords( string reviewerID )
{
   DataSourceSelectArguments sa = new DataSourceSelectArguments();
   ReviewsDataSource.DataSourceMode = SqlDataSourceMode.DataReader;
   ReviewsDataSource.SelectParameters.Clear();
   ReviewsDataSource.SelectParameters.Add( "ReviewerID", reviewerID );
   SqlDataReader rdr = ReviewsDataSource.Select( sa ) as SqlDataReader;

   if ( rdr.HasRows == false )
      return true;

   rdr.Close();

   ReviewsDataSource.DeleteParameters.Clear();
   ReviewsDataSource.DeleteParameters.Add( "ReviewerID", reviewerID );
   int numDeleted = 0;
   string msg = string.Empty;
   try
   {
      numDeleted = ReviewsDataSource.Delete();
   }
   catch ( Exception ex )
   {
      msg = ex.Message + ". ";
   }
   msg += numDeleted.ToString() + " deleted.";
   lblMessage.Text = msg;
   if ( numDeleted == 0 )
      return false;
   else
      return true;
}

Like many projects, the hardest part of this one is getting started. With this much done, the next few steps are almost automatic (get all the reviews for each ASIN listed, for each review obtained get the user ID, get all the reviews by that user, put it all in the DB.). The interesting part will come when it is time to score the users (do we do that dynamically or do we put the score in the DB, and how can we optimize all these calls to Amazon and to the DB and do we care?).

Another interesting set of questions will be to what degree we allow the user to control the scoring and/or the filtering when finding books highly rated by people who "match" the user's ratings of books they've both reviewed. As a quick example, suppose I have the following three users, A, B, and C. It turns out that A and B have shared reviews on only two books, but both awarded the same number of stars to those two books. A and C, however, have rated ten books in common, but their ratings tend to differ by one star in each case. Who is the better match for A: B or C?

All of these questions will be answered in future columns, but feel free to respond with suggestions, criticisms, complaints, and long diatribes about how if you had written this in PERL or (pick your choice) you'd have been done ages ago and already be reading your newly recommended books.


Footnotes

[1] There are some who argue that "data" is a plural noun and one should write "data are," but no American would ever say that if there weren't an editor or a teacher nearby.

[2] Amazon has changed from using the older-style keys that I use to a newer style. Please adjust your code accordingly if you have a new account.

Jesse Liberty is a senior program manager for Microsoft Silverlight where he is responsible for the creation of tutorials, videos and other content to facilitate the learning and use of Silverlight. Jesse is well known in the industry in part because of his many bestselling books, including O'Reilly Media's Programming .NET 3.5, Programming C# 3.0, Learning ASP.NET with AJAX and the soon to be published Programming Silverlight.


Read more Liberty on Beta 2 columns.

Return to WindowsDevCenter.com

Copyright © 2009 O'Reilly Media, Inc.