- Google Message-Handling Basics
- Silverlight Message-Handling Basics
- Handling the HTTP Methods
- HTTP DELETE
- Ready for Our Closeup!
Handling the HTTP Methods
With that background in place, let's look at handling the various HTTP methods from both the Silverlight and Python sides of the conversation. We'll also cover a fair share of other common items in GET that frame how the remaining messages work.
HTTP GET
A GET is a request for a resource. Because it has no content in the request body, a GET request is incredibly simple. Upon load, the Silverlight control sends out a request to the originating server, asking for the list of images for the current logged-in user. In Silverlight, everything that may take time has to be asynchronous, which keeps the user interface (UI) responsive and users happy. (It also keeps people from saying, "Silverlight is slow.")
When our web form came up, it added a handler to the Loaded event to call LoadImages(). In turn, LoadImages executes an HTTP GET against the ImageListService URI and returns a list of images. Let's step through this activity, looking at each piece as it executes.
void LoadImages() { try { WebRequest request = WebRequest.Create(ImageListService); request.BeginGetResponse(ImagesResponse, request); } catch (Exception ex) { Debug.WriteLine(ex.ToString()); } }
As soon as request.BeginGetResponse() is executed, the message goes from the Silverlight application to GAE. When that response returns (or times out), the ImagesResponse method is called with a reference to the original request.
Now we're over in GAE land. Here, a pair of data types gives us access to all of our data. We have an owner type, named Owner, that keeps track of the owner name. The type derives from google.appengine.ext.db.Model. All Model types can contain values, and they all have a globally unique identifier named key. The key comes from the base class:
class Owner(db.Model): name=db.StringProperty()
An Owner acts as the parent for another custom type, ImageFile. The Python ImageFile object, which stores data about images, knows the following:
- The Owner
- Image metadata (caption and description)
- Visibility of the image (public or private)
- The image itself
It has the following declaration:
class ImageFile(db.Model): owner = db.ReferenceProperty(Owner) caption = db.StringProperty() description = db.TextProperty() image = db.BlobProperty() date = db.DateTimeProperty(auto_now_add=True) public = db.BooleanProperty()
When the HTTP GET request comes in, asking for a list of images, the ImageList class responds through its GET method:
class ImageList(webapp.RequestHandler): def get(self): username = "" # Get the current user owner = getCurrentUserOwner() if (not owner is None): # The user is known. Fetch the first 1000 photos images = db.GqlQuery( "SELECT * FROM ImageFile WHERE owner=:1", owner).fetch(1000, 0) imageList = [] # Create temp objects that only # contain the info we need to be sent to # the caller. for image in images: key = image.key() temp = {'key': str(key), 'caption': str(image.caption), 'description':str(image.description), 'public': str(image.public)} imageList.append(temp) # Write the objects to a string and send them on # back writer = JsonWriter() imageResponse = writer.write(imageList) self.response.headers['Content-Type'] = \ "application/json" self.response.out.write(imageResponse)
That last line sends the response back to our Silverlight application, which is patiently waiting for the news about images. When the response arrives, the code needs to put the image information from Python into a format suitable for consumption in .NET. (I call the type ImageItem because it's smaller than and slightly different from ImageFile on the Python side.) We also need to give the object a bit of a hand so that it can explain how to read and write itself to the .NET serialization framework.
[DataContract] public class ImageItem : INotifyPropertyChanged { public static string BaseUri { get; set; } public ImageItem() { } public string ImageUrl { get { return string.Format( "{0}?id={1}", BaseUri, ImageId); } } string _caption; [DataMember(Name = "caption")] public string Caption { get { return _caption; } set { _caption = value; NotifyChange("Caption"); } } string _description; [DataMember(Name="description")] public string Description { get { return _description; } set { _description = value; NotifyChange("Description"); } } [DataMember(Name = "key")] public string ImageId { get; set; } bool _publicImage; [DataMember(Name="public")] public bool PublicImage { get { return _publicImage; } set { _publicImage = value; NotifyChange("PublicImage"); } } public event PropertyChangedEventHandler PropertyChanged; void NotifyChange(string property) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(property)); } } }
You may have noticed that the type also implements INotifyPropertyChanged. This interface allows the type to communicate changes in values back to any UI that's bound to those values. With this type in hand, we can read the returned JSON into ImageItem:
void ImagesResponse(IAsyncResult result) { WebRequest request = (WebRequest)result.AsyncState; try { WebResponse response = request.EndGetResponse(result); Stream responseStream = response.GetResponseStream(); // Get a serializer DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(ImageItem[])); // Read the data into an array ImageItem[] imageItems = ser.ReadObject(responseStream) as ImageItem[]; // Update the UI ImageDocument = new ObservableCollection<ImageItem>(); foreach (ImageItem item in imageItems) { ImageDocument.Add(item); } Dispatcher.BeginInvoke(delegate { lstImages.DataContext = ImageDocument; }); } catch (WebException we) { HttpWebResponse response = (HttpWebResponse)we.Response; Debug.WriteLine(we.ToString()); response.Close(); } catch (Exception e) { Dispatcher.BeginInvoke(delegate { MessageBox.Show(e.ToString()); }); } }
That completes the loop for an HTTP GET.
Next up: What about adding an image?
HTTP POST
When adding a new item, we need to transfer data from the client to the server. Two HTTP methods allow for content in the request body: PUT and POST.
If we're looking at pure REST, creation happens with a POST and updates with a PUT. In practice, PUT and POST functionality typically are merged, for a couple of reasons:
- HTML 4.01 forms limit <form> tags to two method attribute values: GET and POST.
- Many developers prefer to have code that appears as a Save and handles the create/update logic at a lower level.
Our application uses POST in two places: when adding a new image, and when updating the image metadata. When uploading a binary image, the user clicks the Add Images button in the UI. After the user selects a file from the local machine, the Silverlight UI loads the image and sends it off to the server. The code reads the file from the OpenFileDialog at this point. The application can't just save the filename to send the file later; the user gave permission to read the file by selecting it via the OpenFileDialog, so we read the file and store its contents in memory:
private void AddImages_Click(object sender, RoutedEventArgs e) { // Let the user pick a JPEG or PNG to send. OpenFileDialog ofd = new OpenFileDialog(); ofd.Multiselect = false; ofd.Filter = "JPEG Files|*.jpeg;*.jpg|PNG Files|*.png"; bool? result = ofd.ShowDialog(); if (result.HasValue && result.Value) { // Read the file. Stream stream = ofd.File.OpenRead(); byte[] bytes = new byte[ofd.File.Length]; stream.Read(bytes, 0, bytes.Length); stream.Close(); // Send the file to the application. WebRequest request = WebRequest.Create(ImageListService); request.Method = "POST"; request.ContentType = "image"; request.BeginGetRequestStream( GetAddImageRequestStreamCallback, new object[] { request, bytes }); } }
When the callback returns, we have access to the request stream and can send the file to the server. The application grabs the request stream and writes the image to the stream.
After writing the image, we Close() the stream:
void GetAddImageRequestStreamCallback(IAsyncResult result) { object[] stateVariables = (object[])result.AsyncState; WebRequest request = (WebRequest)stateVariables[0]; byte[] image = (byte[])stateVariables[1]; try { Stream stream = request.EndGetRequestStream(result); stream.Write(image, 0, image.Length); stream.Close(); request.BeginGetResponse(AddResponse, request); } catch (WebException we) { HttpWebResponse response = (HttpWebResponse)we.Response; Debug.WriteLine(we.ToString()); response.Close(); } }
This message to add an image then goes over to the ImageList endpoint in our GAE application. There, we read in the image, resize it as a 300 [ts] 300 image, and store it to the data store. If the bytes sent up don't represent a JPEG or PNG image, the code below fails and nothing is stored; the logic in the web application returns an error indicating what went wrong.
In this case, the error will only make sense to those who also develop for the website. Because we don't intend to share this endpoint with other consumers, that limitation is perfectly okay. (If you expose your endpoints for consumption by others, you should catch any exceptions and provide meaningful diagnostics about what went wrong.)
def post(self): currentUser = getCurrentUserOwner() if currentUser is None: return image = ImageFile() uploaded = self.request.body image.image = db.Blob(images.resize(uploaded, 300, 300)) image.owner = currentUser image.public = False image.put()
When the response returns, we reload the images and update the display:
void AddResponse(IAsyncResult result) { WebRequest request = (WebRequest)result.AsyncState; try { WebResponse response = request.EndGetResponse(result); LoadImages(); } catch (WebException we) { HttpWebResponse response = (HttpWebResponse)we.Response; Debug.WriteLine(we.ToString()); response.Close(); } }
Once the user adds the image, she might want to update the image metadata. When the selected image changes, the application sends any changes back to the server. The GAE application only accepts JSON serialized data, so our UI needs to send out the data in JSON format. The endpoint for updates is an individual image endpoint. We bind the selected item to the details view, to make getting and setting properties easy:
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (grdDetails.DataContext != null && chkPublic.IsEnabled) { WebRequest request = WebRequest.Create(ImageService); request.Method = "POST"; request.ContentType = "application/json"; ImageItem imageItem = (ImageItem)grdDetails.DataContext; request.BeginGetRequestStream( GetUpdateImageRequestStreamCallback, new object[] { request, imageItem }); } grdDetails.DataContext = ((ListBox)sender).SelectedItem; }
A few cycles later, the application executes our callback so that we can send the ImageItem to the GAE side. We use a DataContractJsonSerializer to write the object to the request body:
void GetUpdateImageRequestStreamCallback(IAsyncResult result) { try { object[] stateInfo = (object[])(result.AsyncState); WebRequest request = (WebRequest)(stateInfo[0]); ImageItem imageItem = (ImageItem)(stateInfo[1]); Stream stream = request.EndGetRequestStream(result); DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(ImageItem)); ser.WriteObject(stream, imageItem); stream.Close(); request.BeginGetResponse(UpdateResponse, request); } catch (Exception ex) { Dispatcher.BeginInvoke(delegate { MessageBox.Show(ex.ToString()); }); } }
The GAE side picks up this message and reads it into an existing ImageFile instance, and we load the item from the database. If the item exists, and the image's owner matches the current user, the update happens:
def post(self): reader = JsonReader() currentUser = getCurrentUserOwner() tempImage = reader.read(self.request.body) if not currentUser is None: requestedImage = db.get(tempImage['key']) if requestedImage.owner.key() == currentUser.key(): requestedImage.caption = tempImage['caption'] requestedImage.description = tempImage['description'] requestedImage.public = tempImage['public'] requestedImage.put()
An empty response comes back from the update, and we move on.
void UpdateResponse(IAsyncResult result) { WebRequest request = (WebRequest)result.AsyncState; try { WebResponse response = request.EndGetResponse(result); response.Close(); } catch (WebException we) { HttpWebResponse response = (HttpWebResponse)we.Response; Debug.WriteLine(we.ToString()); response.Close(); } }