Sunday, September 5, 2010

Windows Live Writer Integration for Sitecore

Windows Live Writer is a great tool for blogging. It is simple, fast, and can be customized using plugins written in .NET. You can have web-based interface for adding your blog or articles, but integration with desktop client will not be superfluous as it provides a lot of benefits.
 In this post I'll show how to integrate it with Sitecore CMS using MetaWeblog API -  an application interface  that enables weblog entries to be written, edited, and deleted using web services.

Integration can be divided into two parts:
  1. Implementing MetaWeblog API service members 
  2. Creating and configuring various handlers that will tell Live Writer how to find and use our service

In order to implement MetaWeblog web-service, you need to use some library that will add XML-RPC support to .NET, for example, CookComputing XML-RPC.NET. The following table shows what should be implemented and expected parameters / returned results:


Also, required methods and their interfaces are described here (don't forget to check the sample code). 
I'll briefly review implementation of all interface methods in Sitecore.

At first, you need to create a service class, for example:

class CustomBlogService : XmlRpcService

Creating new blog post

According to the table, the first method is metaWeblog.newPost. It should be defined as

[XmlRpcMethod("metaWeblog.newPost")]
public string NewPost(string blogId, string userName, string password, XmlRpcStruct rpcStruct, bool publish)

Before creating a blog post, it makes sense to check whether user's name and password are correct. If everything is fine, use UserSwitcher to switch the editing context and create an item / fill in it's fields:

using (new UserSwitcher(userName, true))
            {              
                dateCreated = rpcStruct["dateCreated"] == null ? DateTime.Now : (DateTime)rpcStruct["dateCreated"];
                var post = // Create your blog post item here;

                post.Editing.BeginEdit();
                post["Title"] = rpcStruct["title"];
                post["Description"] = rpcStruct["description"]              
                post["CreatedDate"] = Sitecore.DateUtil.ToIsoDate(dateCreated);
                post["Categories"] = GetCategoriesFromStruct(rpcStruct);
                post.Editing.EndEdit();

                if (publish)
                {
                    PublishBlogPost(post);
                }

                return post.ID.ToString();
            }

Note, that rpcStruct["dateCreated"] can be null if user did not select publish date explicitly. We use DateUtil.ToIsoDate() to convert date to format used by Sitecore. GetCategoriesFromStruct() and PublishBlogPost() are helper methods, the first one transforms categories into the list of ID's that can be assigned to the fields like TreeList, Checklist, etc. And the second one handles workflow / publishing.

Editing existing blog post

Now, let's try to edit our blog post. Define a method as

[XmlRpcMethod("metaWeblog.editPost")]
public bool EditPost(string postId, string userName, string password, XmlRpcStruct rpcStruct, bool publish)

And here's the implementation:

using (new UserSwitcher(userName, true))
            {
                dateCreated = rpcStruct["dateCreated"] == null ? DateTime.Now : (DateTime)rpcStruct["dateCreated"];
                var post = /* Get your existing blog post item here */.Versions.AddVersion();
    
                post.Editing.BeginEdit();
                post["Title"] = rpcStruct["title"];
                post["Description"] = rpcStruct["description"]                
                post["CreatedDate"] = Sitecore.DateUtil.ToIsoDate(dateCreated);
                post["Categories"] = GetCategoriesFromStruct(rpcStruct);
                post.Editing.EndEdit();

                if (publish)
                {
                    PublishBlogPost(post);
                }

            }

In this example we create a new item version each time user publishes a draft or final version. Live Writer does not support item versions, but I think it makes sense to keep previous versions.

Getting blog post

This one is pretty simple. All you need is to fill in RPC struct and return it. GetPostCategories method returns  categories assigned to your as an array of XmlRpcStruct objects.

[XmlRpcMethod("metaWeblog.getPost")]
public XmlRpcStruct GetPost(string postId, string userName, string password)

/*.... getting blog post from DB, etc. .....*/

var rpcstruct = new XmlRpcStruct
                                    {
                                        {"title", post["Title"]},
                                        {"link", LinkManager.GetItemUrl(post)},
                                        {"description", post["Description"]},
                                        {"dateCreated", post["DateCreated"]},
                                        {"postid", post.ID.ToString()},
                                        {"categories", GetPostCategories(postId)},
                                        {"author", post.InnerItem.Statistics.CreatedBy}
                                    };

                return rpcstruct;

Getting categories

[XmlRpcMethod("metaWeblog.getCategories")]
public XmlRpcStruct[] GetCategories(string blogid, string userName, string password)

Get your categories items, create a XmlRpcStruct array and fill it with your categories:

foreach (var category in categories)
            {
                var rpcstruct = new XmlRpcStruct();
                rpcstruct.Add("categoryid", category.ID);
                rpcstruct.Add("title", category["Title"]);
                rpcstruct.Add("description", category["Description"]);
                categoriesArray[counter] = rpcstruct;
                counter++;
            }
            return categoriesArray;

Getting recent posts

[XmlRpcMethod("metaWeblog.getRecentPosts")]
public XmlRpcStruct[] GetRecentPosts(string blogId, string userName, string password, int numberOfPosts)

In this method you should simply get your recent posts, then fill (see GetPost method) and return XmlRpcStruct  array. I think it does not require sample code.

Deleting post and User Blogs

These two should be really simple

[XmlRpcMethod("blogger.deletePost")]
public bool DeletePost(string appKey, string postId, string userName, string password, bool publish)

[XmlRpcMethod("blogger.getUsersBlogs")]
public XmlRpcStruct[] GetUsersBlogs(string appKey, string userName, string password)

I'll add code sample for GetUserBlogs() only:

var blogList = /* Find all user blogs here*/

            var blogs = new XmlRpcStruct[blogList.Count()];
            foreach (var blog in blogList)
            {
                XmlRpcStruct rpcstruct = new XmlRpcStruct();
                rpcstruct.Add("blogid", blog.ID.ToString());
                rpcstruct.Add("blogName", blog.Name);
                rpcstruct.Add("url", LinkManager.GetItemUrl(blog));
                blogs[counter] = rpcstruct;
                counter++;
            }

NewMediaObject - the most complicated and the last one

It's not a trivial task, but with Sitecore we can do everything :). We get additional benefits here - images will be arranged as we want them to - by blog / year / month / and so on.

[XmlRpcMethod("metaWeblog.newMediaObject")]
public XmlRpcStruct NewMediaObject(string blogId, string userName, string password, XmlRpcStruct rpcStruct)
        {
            string name = rpcStruct["name"].ToString();
            var media = (byte[]) rpcStruct["bits"];
            var blog = Sitecore.Context.ContentDatabase.GetItem(blogId);

            string extenstion = Path.GetExtension(name);
            string imageName = name.Split('.')[0];
            var fileName = imageName + "." + extenstion;

            var options = new MediaCreatorOptions();

            //Your Blog Section / Blog Name / Year / Month / LiveWriter folders
            options.Destination =
                "/sitecore/media library/Blogs/{0}/{1}/{2}/{3}/{4}".FormatWith(blog.Parent.Name,
                                                                                blog.Name,
                                                                                DateTime.Now.Year,
                                                                                DateTime.Now.Month,
                                                                                imageName);
            options.Database = Sitecore.Configuration.Factory.GetDatabase("master");
            options.AlternateText = fileName;

            var rpcStruct = new XmlRpcStruct();
            using (new UserSwitcher(userName, true))
            {
                MediaItem mediaItem;
                using (new UserSwitcher(userName, true))
                {
                    using (var ms = new MemoryStream(media))
                    {
                        mediaItem = MediaManager.Creator.CreateFromStream(ms, fileName, options);
                    }
                }
                rpcStruct.Add("url", MediaManager.GetMediaUrl(mediaItem));
            }
            return rpcStruct;
         }

I've added full method implementation, as it can be reused in any Sitecore project. We build a folder structure and pass it to the Sitecore API, that will create all required folders and media item.

Configuring service handlers

First of all, you need to add "EditUri" section to your blog <head> section:

<link href="http://your sitecore website</u>/rsd.ashx?blogId={561A71CA-3063-4F07-AD13-C9652607BE18}" rel="EditURI" title="RSD" type="application/rsd+xml"></link>

Rsd.ashx is a handler that returns XML describing your blog editing features. CustomBlogService.ashx is a web-service that we've just implemented above.

<?xml version="1.0" encoding="utf-8" ?> 
 <rsd version="1.0">
 <service>
  <engineName>Sample Blog Module</engineName> 
  <engineLink>http://your sitecore website/</engineLink> 
  <homePageLink>http://your sitecore website/</homePageLink> 
 <apis>
  <api name="MetaWeblog" preferred="true" apiLink="http://your sitecore website/CustomBlogService.ashx" blogID="{561A71CA-3063-4F07-AD13-C9652607BE18}" /> 
  </apis>
  </service>
  </rsd>

Now, let's see what we've got

Easily add rich-text blog post with Windows Live gallery, YouTube videos, etc.


Click the "Publish" button and in a few seconds content will be transferred to Sitecore


With all images arranged by blog / date



Nice, isn't it? Live Writer has some limitations, but on the other hand - it keeps blog posting easy, you don't have to care about folder structure of your blog posts / images, or learn some additional stuff. You just write a blog post, add images / videos / maps / galleries and click "Publish". Done!

3 comments:

  1. Very cool, as always...

    Will this be a shared source module?

    ReplyDelete
  2. Thanks Lars. I did not plan to release it as a shared source module, as this is more integration guide than a complete solution.
    But I will attach all sources to the post a bit later.

    ReplyDelete
  3. Very nice post Alex. And for those that don't want to do the integration themselves and just want to write blogs in Sitecore using Live writer, look no further than the EviBlog shared source module: http://trac.sitecore.net/EviBlog

    ReplyDelete