Posted on March 22, 2008 12:24 by mcollins

On occasion, I have needed to write web applications for personal use or for my employers or clients.  These web applications have done different things, from being a full-fledged ERP application to being a personal website (my arch-nemesis) to being a website for an actual company.  One constant function that always seems to pop up for websites and web applications is the concept of content management.

(I should probably qualify this post by saying that I define a website as being what we normally expect a site to be.  For example, this blog is a website.  MSNBC News is a website.  However, there's an administrative piece to this blog, which I would consider a web application.  An ERP application with a web front end is a web application.)

In discussing content management, the first question to pop up is based around keeping content up to date.  "How do we update existing content?"  "How do we publish new items to the site?" "Can our marketing department do this?"

The good news to all of these topics is yes, but it requires some work from the developers to implement.  We live in a golden age right now in that the web has finally matured, and some of the desktop tools are starting to catch up.  In the earlier days of the web, users had to type the new content into a web form.  WYSIWYG editors based on MSHTML for example started showing up to allow users to do rich text formatting inside of a web page.  That was a step forward, but it really wasn't what everyone wanted to get to.

Now in the present, we have two really good tools for content management and publishing: Windows Live Writer from Microsoft and Microsoft Office Word 2007.  Both tools offer an ideal solution (and Windows Live Writer offers a better solution).  Both tools let content writers create, proof, and edit their content on their desktop or laptop machines, and when the content is ready to be published, both tools can communicate with web applications that implement open standards to publish the content to the websites.

So, how does this content publishing get done?  The answer is in the standards that other applications are using.  We could always build our own interface, but the advantages are that we don't need to rebuild Windows Live Writer if we follow the other open standards.  The most popular (in my opinion) standards right now are based on an XML-RPC protocol called the MetaWeblog API.  Using MetaWeblog, a tool such as Word or Windows Live Writer can make HTTP calls to the website and transfer the text for content plus any associated images.  MetaWeblog is a standard that developed from another earlier API that was developed for the Blogger blogging service.  MetaWeblog fixed some of the shortcomings of the API, but rather than replace the Blogger API, MetaWeblog enhanced it while leaving some of the functionality of the Blogger API in use.  In my next post, we'll take a look at the MetaWeblog API.  In this post however, we're going to look at the Blogger API.

First, my implementation of the Blogger API is using XML-RPC.NET from Cook Computing.  This is an excellent .NET implementation of the XML-RPC protocol and integrates very nicely into any ASP.NET website or web application.

The Blogger API specification can be found here.

The Blogger API defines seven operations that can be used by clients:

  • blogger.deletePost: deletes an existing post from a blog
  • blogger.editPost: edits an existing post on a blog
  • blogger.getTemplate: retrieves the text for the blog template for use in WYSIWYG editors
  • blogger.getUserInfo: retrieves the name of a user and the user's email address from the blog
  • blogger.getUsersBlogs: retrieves the list of blogs on the website that the user can post to
  • blogger.newPost: publishes a new post to a blog
  • blogger.setTemplate: uploads a new HTML template for a blog

 

Now, reading through this list of operations, two words are going to stick out to most readers: "post" and "blog."  You may now be asking yourself, is this really relevant to me because I'm not writing a blog.  The answer is yes, if you think of this API and these terms in abstract terms (couldn't come up with a better word than repeating terms...it's a Saturday morning...you can't expect too much from me).

So if I'm not writing a blog, can I still use the Blogger API?  Sure.  Think about it from the concept of another application.  What if you're building a knowledge base or a FAQ list for a customer.  They're not blogs, but they store content like a blog.  And a post could be a question on an FAQ, or a knowledge base item on a knowledge base, couldn't it?  Or if you're writing a news website, couldn't the "blog" be the news website and a post be an article?  The answer to al of these questions is yes.  You just need to fit these terms into the content management solution that you're building.

Back to our Blogger API implementation. the first thing that we want to do with our new website is to publish content to the site.  So we're going to start out with defining the blogger.newPost operation to allow us to publish contet to a website using the Blogger API.  Here's how that method is defined:

   1: [XmlRpcMethod("blogger.newPost",
   2:     Description = "Makes a new post to a blog.")]
   3: string NewPost(string appKey, string blogId, string userName,
   4:     string password, string content, bool publish);

 

In the above code sample, I defined a method named NewPost which will implement the blogger.newPost operation.  You'll also see on line 1, that I decorated the NewPost method with the XmlRpcMethodAttribute attribute.  The XmlRpcMethodAttribute class is defined by XML-RPC.NET and allows me to bind an XML-RPC operation to a .NET method.  When we actually add this method to an XML-RPC web service later in this post, any XML-RPC requests received by our web application requesting the method blogger.newPost will be directed to our NewPost implementation.

The NewPost method takes six parameters.  The appKey parameter is for the most part ignored.  I think that back in the early days of Blogger, Blogger probably assigned unique keys to each client application that had signed up to use the XML-RPC API.  Over time, I'm guessing that they moved away from this practice, which is why this key is not used.  It's very safe to go ahead and ignore this parameter.

The blogId parameter is the unique identifier of the blog where the new content should be posted to.  Again, if you're not writing a blog, this could be a unique identifier you assign to your website, a category, some sub-application, etc.  The meaning of blogId is open for you to determine in your implementation.

The userName and password parameters are obviously the user name and password of the user publishing the content to the website.  You could use these fields in an ASP.NET application for example to verify the user against the ASP.NET membership database or your proprietary user store.

The content parameter is the important stuff.  This parameter will contain the text or HTML text of the content being published to the website.  This is obviously the data that you're going to store and display on your web pages.

The publish parameter is a boolean parameter that is set to true if the author wants to make the content immediately available on the website, or save the content to be published at a later time if this parameter is set to false.  When I implement this interface, I usually have a binary field on my content table that I store this value in.  I'll then use that binary field to determine what content gets displayed on my web page.

The return value for the blogger.NewPost operation is the unique identifier for the new post, if the new post was published or saved to the blog (or website) successfully.

Assuming that we've successfully published a new post up to the website, what happens if we discover an error that we want to fix and update the website with the corrected content?  This is where the blogger.editPost operation comes into play:

   1: [XmlRpcMethod("blogger.editPost",
   2:     Description = "Changes the contents of the specified post.")]
   3: bool EditPost(string appKey, string postId, string userName,
   4:     string password, string content, bool publish);

 

The parameters for the EditPost operation look pretty similar to the NewPost operation, so I won't rehash them.  The only difference is that where NewPost accepted a blogId identifier to identify the blog that the new post was being published to, EditPost accepts the unique identifier of the post being edited.  This is the same value that was returned by NewPost as its return value.  All of the other parameters are the same.

The publish parameter of EditPost, serves the same purpose as in NewPost.  One reason why it's here on the EditPost operation too is that the author may not have published the new content when NewPost was called, but now the author wants to publish the content on the site.  The author can use EditPost to change the value of the "is published" flag.

The return value for EditPost is always true.  If an error occurred in editing or updating the post, then the XML-RPC operation should return a fault, which XML-RPC.NET will turn into a .NET exception.

On to our third operation: blogger.deletePost.  Obviously, this operation is used to delete a post that has already been published on the blog.

   1: [XmlRpcMethod("blogger.deletePost",
   2:     Description = "Deletes a post from the blog.")]
   3: bool DeletePost(string appKey, string postId, string userName,
   4:     string password, bool publish);

 

The only parameter here that doesn't make sense if the publish parameter.  It can be safely ignored.  Again, this operation will always return true, or will return a fault if an error occurs.

The next operation of interest is blogger.getUsersBlogs.  This operation is used in a multi-blog scenario where an author has the capability of publishing to many blogs or content areas.  Recall that the blogger.newPost operation requires the unique identifier of a blog to publish to?  Where does a client application get a list of the available blogs?  blogger.getUsersBlogs.

   1: [XmlRpcMethod("blogger.getUsersBlogs",
   2:     Description = "Returns information about all of the blogs that " +
   3:     "the user is a member of.")]
   4: Blog[] GetUsersBlogs(string appKey, string userName, string password);

 

The return value of blogger.getUsersBlogs is an array of structures containing information about the available blogs.  This structure looks like the following:

   1: [Serializable]
   2: [XmlRpcMissingMapping(MappingAction.Error)]
   3: public class Blog {
   4:     /// <summary>
   5:     /// The unique identifier of the blog.
   6:     /// </summary>
   7:     [SuppressMessage("Microsoft.Design", "CA1051",
   8:         Justification = "Public fields must be exposed for XML-RPC.NET")]
   9:     [SuppressMessage("Microsoft.Naming", "CA1704",
  10:         Justification = "The field name is specified by the Blogger API")]
  11:     public string blogid;
  12:  
  13:     /// <summary>
  14:     /// The name of the blog.
  15:     /// </summary>
  16:     [SuppressMessage("Microsoft.Design", "CA1051",
  17:         Justification = "Public fields must be exposed for XML-RPC.NET")]
  18:     public string blogName;
  19:  
  20:     /// <summary>
  21:     /// The URL where the blog is hosted.
  22:     /// </summary>
  23:     [SuppressMessage("Microsoft.Design", "CA1051",
  24:         Justification = "Public fields must be exposed for XML-RPC.NET")]
  25:     public string url;
  26: }

 

This structure has three fields: blogid, blogName, and url.  The blogid field is the blog's unique identifier that is sent to blogger.newPost.  The blogName parameter is something descriptive such as the title of the blog.  The url parameter is the URL of the home page for the blog.

A couple of .NET implementation notes, the structure doesn't have to be serializable, but I decorated the type with the SerializableAttribute attribute for my own use internally to my application.  Also, I added the SuppressMessageAttribute attributes to the structure to get around the FxCop errors that were being raised when validating this code.  At the time that I wrote this, XML-RPC.NET can only read public fields when serializing/deserializing XML-RPC strucutres.  I don't understand why public properties were not acceptable, but this is the way that it works.  I guess that I could always fix XML-RPC.NET, but I'm not losing sleep over this little detail and I want to implement the Blogger API, not rewrite XML-RPC.NET when there's a workaround that I can live with.

Also, I chose a class instead of a structure because internally I might build the list of blogs up in a collection type such as a List.  Reference types such as objects work better with these collection types than value-based types such as structs do.  That was my reasoning behind using a class and not a struct.

The next operation of interest is blogger.getUserInfo.  This operation can be called by a blogging client to get metadata about the blogger.

   1: [XmlRpcMethod("blogger.getUserInfo",
   2:     Description = "Returns information about the specified user.")]
   3: User GetUserInfo(string appKey, string userName, string password);

 

The User structure used by this method is defined below:

   1: [Serializable]
   2: [XmlRpcMissingMapping(MappingAction.Error)]
   3: public class User {
   4:     /// <summary>
   5:     /// The unique identifier for the user.
   6:     /// </summary>
   7:     [SuppressMessage("Microsoft.Design", "CA1051",
   8:         Justification = "Public fields must be exposed for XML-RPC.NET")]
   9:     [SuppressMessage("Microsoft.Naming", "CA1704",
  10:         Justification = "The field name is specified by the Blogger API")]
  11:     public string userid;
  12:  
  13:     /// <summary>
  14:     /// The user's first name.
  15:     /// </summary>
  16:     [SuppressMessage("Microsoft.Design", "CA1051",
  17:         Justification = "Public fields must be exposed for XML-RPC.NET")]
  18:     [SuppressMessage("Microsoft.Naming", "CA1704",
  19:         Justification = "The field name is specified by the Blogger API")]
  20:     public string firstname;
  21:  
  22:     /// <summary>
  23:     /// The user's last name.
  24:     /// </summary>
  25:     [SuppressMessage("Microsoft.Design", "CA1051",
  26:         Justification = "Public fields must be exposed for XML-RPC.NET")]
  27:     [SuppressMessage("Microsoft.Naming", "CA1704",
  28:         Justification = "The field name is specified by the Blogger API")]
  29:     public string lastname;
  30:  
  31:     /// <summary>
  32:     /// The user's nick name.
  33:     /// </summary>
  34:     [SuppressMessage("Microsoft.Design", "CA1051",
  35:         Justification = "Public fields must be exposed for XML-RPC.NET")]
  36:     public string nickname;
  37:  
  38:     /// <summary>
  39:     /// The user's email address.
  40:     /// </summary>
  41:     [SuppressMessage("Microsoft.Design", "CA1051",
  42:         Justification = "Public fields must be exposed for XML-RPC.NET")]
  43:     public string email;
  44:  
  45:     /// <summary>
  46:     /// The URL for the user's website or blog.
  47:     /// </summary>
  48:     [SuppressMessage("Microsoft.Design", "CA1051",
  49:         Justification = "Public fields must be exposed for XML-RPC.NET")]
  50:     public string url;
  51: }

 

Again, I made the same implementation decisions that I did with the above Blog structure.

The final two methods are used by WYSIWYG editors to download or replace the templates being used by the blogs.  As far as I can tell, these operations aren't being used by any current clients, however, here they are for completeness to the specification:

   1: [XmlRpcMethod("blogger.getTemplate",
   2:     Description = "Returns the text of the main or archive index " +
   3:     "template for the specified blog.")]
   4: string GetTemplate(string appKey, string blogId, string userName,
   5:     string password, string templateType);
   6:  
   7: [XmlRpcMethod("blogger.setTemplate",
   8:     Description = "Changes the template for the specified blog.")]
   9: bool SetTemplate(string appKey, string blogId, string userName,
  10:     string password, string templateText, string templateType);

 

The GetTemplate operation returns the text for the specified template.  The templateType parameter is going to identify which template to return.  According to the specification, there may be two templates.  The first template, identified by the type "main," returns the HTML for the home page of the blog.  The second template, identified by the type "archiveIndex," returns the HTML for a page that shows the summary of past posts on the blog.

In the SetTemplate operation, the parameter templateText contains the new HTML content for the home page or archive page, as indicated by the templateType parameter.  Like the other methods above, SetTemplate will always return true unless an error occurs and a fault is returned.

Here's the complete definition of my Blogger API operations, defined in a .NET interface that we'll use in a later post to implement these methods:

   1: /// <summary>
   2: /// Defines the XML-RPC operations that are supported by the Blogger
   3: /// API for clients to use to publish content to a website or blog.
   4: /// </summary>
   5: public interface IBloggerService {
   6:     /// <summary>
   7:     /// Deletes a post from the blog.
   8:     /// </summary>
   9:     /// <param name="appKey">
  10:     /// The unique identifier or passcode of the application deleting
  11:     /// the post. This parameter is typically ignored.
  12:     /// </param>
  13:     /// <param name="postId">
  14:     /// The unique identifier of the post to be deleted.
  15:     /// </param>
  16:     /// <param name="userName">
  17:     /// The login for a user that can delete the post.
  18:     /// </param>
  19:     /// <param name="password">The user's password.</param>
  20:     /// <param name="publish">This parameter is ignored.</param>
  21:     /// <returns>
  22:     /// Returns true if the post was successfully deleted.
  23:     /// </returns>
  24:     [XmlRpcMethod("blogger.deletePost",
  25:         Description = "Deletes a post from the blog.")]
  26:     bool DeletePost(string appKey, string postId, string userName,
  27:         string password, bool publish);
  28:  
  29:     /// <summary>
  30:     /// Changes the contents of the specified post.
  31:     /// </summary>
  32:     /// <remarks>
  33:     /// If <paramref name="publish"/> is set to true, then the post
  34:     /// should be published.
  35:     /// </remarks>
  36:     /// <param name="appKey">
  37:     /// The unique identifier or passcode of the application sending the
  38:     /// post. This parameter is typically ignored.
  39:     /// </param>
  40:     /// <param name="postId">
  41:     /// The unique identifier of the post that will be changed.
  42:     /// </param>
  43:     /// <param name="userName">
  44:     /// The login for a user who has permission to post to the blog.
  45:     /// </param>
  46:     /// <param name="password">
  47:     /// The password for the user.
  48:     /// </param>
  49:     /// <param name="content">
  50:     /// The contents of the post.
  51:     /// </param>
  52:     /// <param name="publish">
  53:     /// True if the post should be published immediately.
  54:     /// </param>
  55:     /// <returns>
  56:     /// Returns true if the post was successfully updated.
  57:     /// </returns>
  58:     [XmlRpcMethod("blogger.editPost",
  59:         Description = "Changes the contents of the specified post.")]
  60:     bool EditPost(string appKey, string postId, string userName,
  61:         string password, string content, bool publish);
  62:  
  63:     /// <summary>
  64:     /// Returns the text of the main or archive index template for the
  65:     /// specified blog.
  66:     /// </summary>
  67:     /// <param name="appKey">
  68:     /// The unique identifier or passcode of the application sending the
  69:     /// post. This parameter is typically ignored.
  70:     /// </param>
  71:     /// <param name="blogId">
  72:     /// The unique identifier of the blog whose template is to be returned.