Tuesday, 17 May 2011

Real world Orchard CMS – part 5 – caching - lets get that twitter widget working.

As I said in my earlier post, this series is about creating a real world site with Orchard CMS, I’m not covering using Orchard to manage the site, just the technical development parts. All of the code is available on codeplex here : http://orchardsamplesite.codeplex.com/

Preamble

So far we’ve created a basic theme, created a twitter widget to display the latest twitter feeds and gone back and tidied up some parts of our theme that weren’t working out for us. In this post I’m going to quickly go back and tidy up a loose end that’s been bugging me - getting the twitter widget doing something useful. We originally created that part to just return some canned results, but I figure going back to address this is a good exercise as we can look at caching in Orchard, so without further ado, our twitter widget will shortly look like this;

image

Getting tweets

First off we need to get our ITwitterService concrete implementation to go to twitter and get the latest tweets for the person we’ve specified. Open the TwitterService.cs file in the services folder of the module project and change it as follows;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Orchard.Environment.Extensions;
using SampleSiteModule.Models;

namespace SampleSiteModule.Services
{
    [OrchardFeature("TwitterWidget")]
    public class TwitterService : ITwitterService
    {
        const string FeedUrl = "http://api.twitter.com/1/statuses/user_timeline/{0}.rss";

        /// <summary>
        /// gets the latest tweets based on the configuration specified in the TwitterWidgetPart.
        /// Uses Linq2XML to load the feed url and select the tweets.
        /// </summary>
        /// <param name="twitterUserName">Name of the twitter user.</param>
        /// <param name="maxPosts">The max posts.</param>
        /// <returns></returns>
        public IList<Tweet> GetLatestTweetsFor(string twitterUserName, int maxPosts)
        {
            XDocument doc = XDocument.Load(String.Format(FeedUrl, twitterUserName));
            return (from item in doc.Descendants("item") 
                         select new Tweet
                         {
                             Text = TrimUserFrom((string) item.Element("title")),
                             DateStamp = (DateTime) item.Element("pubDate"),
                             Link = (string) item.Element("link")
                         })
                         .Take( maxPosts )
                         .ToList();
        }

        private string TrimUserFrom(string tweet)
        {
            int position = tweet.IndexOf(':');
            if (position == -1) return tweet;

            return tweet.Substring(position + 1);
        }
    }
}

I’ve highlighted the key parts – our method signature is slightly different (It felt wrong to pass a part across the boundary), and we’re using Linq2XML to get the latest tweets in RSS format from twitter for the username specified, we execute a simple linq query to get the items and construct a list of Tweet objects, all very straight forward. Notice that the service itself isn’t concerning itself here with caching, we’re going to take care of caching in the driver in this instance.

Modify the driver to use caching

using System;
using Orchard.Caching;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Environment.Extensions;
using Orchard.Services;
using SampleSiteModule.Models;
using SampleSiteModule.Services;

namespace SampleSiteModule.Drivers
{
    /// <summary>
    /// Content part driver for the twitter widget part
    /// </summary>
    [OrchardFeature("TwitterWidget")]
    public class TwitterWidgetDriver : ContentPartDriver<TwitterWidgetPart>
    {
        private const string TwitterUserCacheKey = "TwitterWidgetPostsFor-{0}";
        private readonly ITwitterService _twitter;
        private readonly ICacheManager _cache;
        private readonly IClock _clock;

        public TwitterWidgetDriver(ITwitterService twitter, ICacheManager cache, IClock clock)
        {
            _twitter = twitter;
            _cache = cache;
            _clock = clock;
        }

        protected override DriverResult Display(TwitterWidgetPart part, string displayType, dynamic shapeHelper)
        {
            // Get tweets, from the cache or from twitter
            var tweets = _cache.Get(String.Format(TwitterUserCacheKey, part.TwitterUserName), ctx => 
                {
                    ctx.Monitor( _clock.When( TimeSpan.FromMinutes(part.CacheMinutes)));
                    return _twitter.GetLatestTweetsFor(part.TwitterUserName, part.MaxPosts);
                });

            return ContentShape("Parts_TwitterWidget",
                () => shapeHelper.Parts_TwitterWidget(
                        TwitterUserName: part.TwitterUserName ?? String.Empty,
                        Tweets: tweets));
        }

        protected override DriverResult Editor(TwitterWidgetPart part, dynamic shapeHelper)
        {
            return ContentShape("Parts_TwitterWidget_Edit",
                () => shapeHelper.EditorTemplate(
                    TemplateName: "Parts/TwitterWidget",
                    Model: part,
                    Prefix: Prefix));
        }

        protected override DriverResult Editor(TwitterWidgetPart part, IUpdateModel updater, dynamic shapeHelper)
        {
            updater.TryUpdateModel(part, Prefix, null, null);
            return Editor(part, shapeHelper);
        }
    }
}

This is almost the same as it was before, but we take two new dependencies -  an ICacheManager and an IClock. In the display method we then use _cache.Get which will inspect the cache to see if we have data that is already cache, within the duration specified, for the twitter user provided in the twitter widget configuration. If the cache determines that it doesn’t have cached data, it then executes the Func provided which in turn determines how to invalidate the cache (in this case when the clock has ticked over the specified number of minutes) and calls the twitter service as normal. The remainder of the code is as before.

Finally, make it look a bit prettier

The Views/Parts/TwitterWidget.cshtml file then changes as;

@using SampleSiteModule.Models

@if (Model.Tweets.Count < 1)
{
    <p>There are no tweets for @Model.TwitterUserName</p>
}
else
{
    foreach (Tweet tweet in Model.Tweets)
    {
        <p><a href="@tweet.Link" target="_blank">@tweet.Text</a>
            <br/>
            @tweet.FriendlyDate
        </p>            
    }
}

Final words

Easy huh? I’ve made a few other minor modifications to the source, mostly in structure and naming (eg: putting drivers and handlers into more sensible folders and renaming a couple of files), but nothing that really affects the build.

As ever, grab the code from codeplex: http://orchardsamplesite.codeplex.com under the part-05 folder (or just skip ahead to final, which is evolving quicker than the blog posts are!).

12 comments:

  1. your a bloody legend. thanks

    ReplyDelete
  2. very nice!
    i've got a question: i'm looking for a calenar module, where i can add appointments in the backend to a site, and there it is displayed as a list. similar to this wordpress plugin: http://wordpress.org/extend/plugins/the-events-calendar/screenshots/

    do you know something i could use?
    i wanted to try code it myself, but i just don't have the time and the knowledge about programming orchard plugins
    thanks

    ReplyDelete
  3. @Anonymous number 1 - thanks ;)

    @Anonymous number 2 - I've not had cause to look for one like this myself yet, but having looked at the wordpress one you're trying to create it shouldn't be too difficult to create.

    Have you had a look on the orchard gallery? http://orchardproject.net/gallery/ a quick search yielded a couple of options, but not used either so not sure they will meet your requirements.

    HTH
    Tony

    ReplyDelete
  4. Gracias amigo!
    The entire series has been very helpful.

    ReplyDelete
  5. Hi Tony,

    Can you extend this example to show how asynchronous controller support can be added to the twitter widget?.

    ReplyDelete
  6. I meant to say asynchronous support to the twitter calling service.

    Our particular use case is to build many widgets like the twitter webservice for different backends. (Consider News, Edgar Filings etc). We would like to put all these widgets in a single page. If orchard were to function synchronously (where each widget calls its respective backend), it would be quite inefficient. Would be very interested to see if the calls can be made asynchronous, so all backend calls can be kicked off as simultaneously as possible.

    ReplyDelete
  7. Hi,

    Yes, I will be covering that topic in a later post, specifically using Ajax to render widgets, which will likely cover what you need...

    Cheers,
    Tony

    ReplyDelete
  8. Thanks Tony. It would be very interesting if you could cover 2 technical solutions.

    1). How to do this asynchronously on the webserver? (For example, http://www.aaronstannard.com/post/2011/01/06/asynchonrous-controllers-ASPNET-mvc.aspx)

    2). How to do this using AJAX?

    -venkat.

    ReplyDelete
  9. Great work tony.

    thanks
    Parminder

    ReplyDelete
  10. Super Sweet Tuts! I'm a total newb to C#, MVC and orchard and I dig how easy you make it to get into a truly real world scenario!

    How would we grab the urls and display them as links for hash tags and shortened url links within tweets?

    ReplyDelete
  11. I'm not finding the code where you said it is: http://orchardsamplesite.codeplex.com/ ... just circular URL references back to this page. Might it be somewhere else? Thanks

    ReplyDelete
  12. Note: If anyone is following along with these tutorials in 2014; Twitter have deprecated their v1 API and you can't access it like this mentions anymore. The only way to get at it is to set up an app within Twitter and then use OpenId to auth it. There are plenty tutorials and no doubt some handy nuget packages out there but I just wanted to point out why its not working if you're getting stuck.

    ReplyDelete