Thursday, 12 May 2011

Real world Orchard CMS – part 3 – creating the twitter widget

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
This post is about creating a widget that will render a list of latest twitter feeds on the site. Rather than complicate matters with calling the twitter API, this post will look at building the widget itself and return canned results. Interfacing to twitter itself is left as an exercise for the reader.

Goal
To create a widget that can be added to a page that will display a list of recent tweets;

Background reading and preparation
If you are following along with the series, you should have already completed part 2 to get your theme up and running. As for background reading;

In a later article we will look at how you can build a widget without a model (content record and part), but for now, this is the standard way of constructing a widget for your site.

Lets get started - codegen the module
Open a command line and navigate to the bin directory of the site source. Here you will find orchard.exe, a command line tool for managing orchard and where we will generate our scaffolding for our custom module. Run orchard.exe. Sometimes I get an exception when I try to run the tool, if the same happens to you, just run it again and it should fire up second time round and you will be presented with the orchard shell.

Use codegen to generate the boilerplate code for your module by executing the command;

codegen module SampleSiteModule

Open the project in visual studio
Back in visual studio you can now add the module project to the orchard solution (Right click the solution name in solution explorer and select Add –> existing project). Find the newly created module project under orchard/modules/SampleSiteModule/SampleSiteModule.csproj and select it, you should then have the module project in your solution;

image

Create the record, part, driver and handler
Our widget, when added to a page, will offer the author the ability to specify the twitter user to get tweets for, the number of tweets to obtain and a duration of time to cache the results for before hitting the twitter API again. For this we need to define the fields in a ContentRecord. Within models create a new file – TwitterWidgetRecord.cs as follows;

using Orchard.ContentManagement.Records;
using Orchard.Environment.Extensions;

namespace SampleSiteModule.Models
{
    [OrchardFeature("TwitterWidget")]
    public class TwitterWidgetRecord : ContentPartRecord
    {
        public virtual string TwitterUser { get; set; }
        public virtual int MaxPosts { get; set; }
        public virtual int CacheMinutes { get; set; }
    }
}

This simply defines the class that will represent the data to be persisted for this widget. It derives from ContentPartRecord, which in turn contains the id’s and what not’s to marry this record up to the rest of the data that composes the overall content record. Next, we need to define a ContentPart that wraps this information. Create a new file (again in models) – TwitterWidgetPart.cs;

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Orchard.ContentManagement;
using Orchard.Environment.Extensions;

namespace SampleSiteModule.Models
{
    [OrchardFeature("TwitterWidget")]
    public class TwitterWidgetPart : ContentPart<TwitterWidgetRecord>
    {
        [Required]
        public string TwitterUserName
        {
            get { return Record.TwitterUser; }
            set { Record.TwitterUser = value; }
        }

        [Required]
        [DefaultValue(5)]
        public int MaxPosts
        {
            get { return Record.MaxPosts; }
            set { Record.MaxPosts = value; }
        }

        [Required]
        [DefaultValue(60)]
        public int CacheMinutes
        {
            get { return Record.CacheMinutes; }
            set { Record.CacheMinutes = value; }
        }
    }
}

Next, the handler to tell orchard that we want to store the TwitterWidgetRecord to the database - create models\TwitterWidgetRecordHandler.cs;

using Orchard.ContentManagement.Handlers;
using Orchard.Data;
using Orchard.Environment.Extensions;

namespace SampleSiteModule.Models
{
    [OrchardFeature("TwitterWidget")]
    public class TwitterWidgetRecordHandler : ContentHandler
    {
        public TwitterWidgetRecordHandler(IRepository<TwitterWidgetRecord> repository)
        {
            Filters.Add(StorageFilter.For(repository));
        }
    }
}

Finally to complete this section we need a driver to build the shapes required to render the widget. Create models\TwitterWidgetDriver.cs;

using System;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Environment.Extensions; 
namespace SampleSiteModule.Models
{
[OrchardFeature("TwitterWidget")] public class TwitterWidgetDriver : ContentPartDriver<TwitterWidgetPart> { // GET protected override DriverResult Display(TwitterWidgetPart part, string displayType, dynamic shapeHelper) { return ContentShape("Parts_TwitterWidget", () => shapeHelper.Parts_TwitterWidget( TwitterUserName: part.TwitterUserName ?? String.Empty, Tweets: null)); } // GET protected override DriverResult Editor(TwitterWidgetPart part, dynamic shapeHelper) { return ContentShape("Parts_TwitterWidget_Edit", () => shapeHelper.EditorTemplate( TemplateName: "Parts/TwitterWidget", Model: part, Prefix: Prefix)); } // POST protected override DriverResult Editor(TwitterWidgetPart part, IUpdateModel updater, dynamic shapeHelper) { updater.TryUpdateModel(part, Prefix, null, null); return Editor(part, shapeHelper); } } }

There is quite a bit missing here yet, we need to come back and revisit this driver to actually get the tweets and build the shape correctly. For now, our Display method is constructing a shape called “Parts_TwitterWidget” that will contain properties for the username and a collection of tweets (presently null). The Editor methods build a different part - “Parts_TwitterWidget_Edit” and point to the template file that will be used to present the form for creating one of these widgets.

Creating the service
To actually get the data from twitter (or the canned results in our case) we need a class to represent a tweet and a service to go and get the data. Create a new folder in your project called Services and add a new file – ITwitterService.cs;

using System.Collections.Generic;
using Orchard;
using SampleSiteModule.Models;

namespace SampleSiteModule.Services
{
    public interface ITwitterService : IDependency
    {
        IList<Tweet> GetLatestTweetsFor(TwitterWidgetPart part);
    }
}

That defines the interface for the service – notice the IDependency, that tells the dependency injection framework (AutoFac) to discover this type and provide concrete implementations of it automatically to anything that declares a dependency on it. Our service just offers a single method to get the list of latest tweets based on the configuration specified in the part record. Create CachedTwitterService.cs in the services folder with the following concrete implementation;

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

namespace SampleSiteModule.Services
{
    [OrchardFeature("TwitterWidget")]
    public class CachedTwitterService : ITwitterService
    {
        public IList<Tweet> GetLatestTweetsFor(TwitterWidgetPart part)
        {
            List<Tweet> results = new List<Tweet>()
            {
                new Tweet{ DateStamp = DateTime.Now.AddSeconds(-10), Text = "Tweet number three" },
                new Tweet{ DateStamp = DateTime.Now.AddMinutes(-10), Text = "Tweet number two" },
                new Tweet{ DateStamp = DateTime.Now.AddDays(-5), Text = "Tweet number one" }
               };

            return results;
        }
    }
}

In this case, we’re just returning some canned results, your implementation should go off to twitter and get the part.MaxPosts tweets for part.TwitterUserName, push it into a cache for part.CacheMinutes.

The Tweet object being added to the list needs to be defined, so add Tweet.cs to your models directory;

using System;

namespace SampleSiteModule.Models
{
    public class Tweet
    {
        public DateTime DateStamp { get; set; }
        public string Text { get; set; }

        public string FriendlyDate
        {
            get
            {
                TimeSpan span = DateTime.Now - DateStamp;
                if (span.TotalSeconds < 30)
                    return "moments ago.";

                if (span.TotalSeconds < 60)
                    return "Less than a minute ago.";

                if (span.TotalMinutes < 60)
                    return String.Format("{0:0} minute{1} ago", span.TotalMinutes, span.TotalMinutes > 1 ? "s" : "");

                if (span.TotalHours < 24)
                    return String.Format("{0:0} hour{1} ago", span.TotalHours, span.TotalHours > 1 ? "s" : "");

                return String.Format("{0:0} day{1} ago", span.TotalDays, span.TotalDays > 1 ? "s" : "");
            }
        }
    }
}

We can now go back and revisit our driver to have a dependency on this service and invoke it to get the model;

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

namespace SampleSiteModule.Models
{
    [OrchardFeature("TwitterWidget")]
    public class TwitterWidgetDriver : ContentPartDriver<TwitterWidgetPart>
    {
        protected ITwitterService Twitter{ get; private set; }

        public TwitterWidgetDriver(ITwitterService twitter)
        {
            Twitter = twitter;
        }

        // GET
        protected override DriverResult Display(TwitterWidgetPart part, string displayType, dynamic shapeHelper)
        {
            return ContentShape("Parts_TwitterWidget",
                () => shapeHelper.Parts_TwitterWidget(
                        TwitterUserName: part.TwitterUserName ?? String.Empty,
                        Tweets: Twitter.GetLatestTweetsFor(part)));
        }

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

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

Dependency injection will take care of getting the concrete implementation of the ITwitterService passed into the constructor. If you’ve not used dependency injection before (where have you been!), then take a look at the numerous frameworks out there including Castle Windsor, AutoFac (used in Orchard), Unity, StructureMap et al.

Migrations and tidying up some loose ends
Before we come to the views to actually render the widget, we should probably create our migration to tell Orchard what data tables we need and how the widget part is defined. Create a migrations.cs file in your project;

using System.Data;
using Orchard.ContentManagement.MetaData;
using Orchard.Core.Contents.Extensions;
using Orchard.Data.Migration;
using Orchard.Environment.Extensions;
using SampleSiteModule.Models;

namespace SampleSiteModule
{
    [OrchardFeature("TwitterWidget")]
    public class Migrations : DataMigrationImpl
    {
        public int Create()
        {
            // ** Version one - create the twitter widget ** //

            // Define the persistence table as a content part record with
            // my specific fields.
            SchemaBuilder.CreateTable("TwitterWidgetRecord", 
                table => table
                    .ContentPartRecord()
                    .Column("TwitterUser", DbType.String)
                    .Column("MaxPosts", DbType.Int32)
                    .Column("CacheMinutes", DbType.Int32));

            // Tell the content def manager that our TwitterWidgetPart is attachable
            ContentDefinitionManager.AlterPartDefinition(typeof(TwitterWidgetPart).Name,
                builder => builder.Attachable());

            // Tell the content def manager that we have a content type called TwitterWidget
            // the parts it contains and that it should be treated as a widget
            ContentDefinitionManager.AlterTypeDefinition("TwitterWidget",
                cfg => cfg
                    .WithPart("TwitterWidgetPart")
                    .WithPart("WidgetPart")
                    .WithPart("CommonPart")
                    .WithSetting("Stereotype", "Widget"));

            return 1;
        }
    }
}

The create method is invoked when the module is first installed, after that, updates are handled by creating methods in the migration named UpdateFromX() where X is the version to update from. In our create method we tell orchard we need a table for our widget record, that it is a content part record and what fields we intend to store. We then tell the content definition manager to set the twitter widget part as something that can be attached to a content type and then create the TwitterWidget content type itself, composed of our new twitter widget part, and the standard widget and common parts. The stereotype setting just lets orchard know that this is a widget type.

As with the theme in the earlier post, we also need a text file to tell orchard about our module and what it exposes. Create a module.txt file in the project;

Name: SampleSiteModule
AntiForgery: enabled
Author: You
Website: http://www.deepcode.co.uk
Version: 1.0
OrchardVersion: 1.0
Description: Provides widgets and features for the sample site module
Category: Sample Site
Features:
    TwitterWidget:
        Name: Twitter Widget
        Category: Sample Site
        Description: Widget for latest tweets

Again, most of this is obvious, but the features section describes the features that are exposed from the module. Modules can expose multiple features, and we’ll be building on this module later to add more and more features so you will notice that all the code above has the OrchardFeature attribute which indicates to orchard what code is relevant to what features. If your module only has a single feature you probably don’t need to bother with this, but as we’re going to build on it, I set it up for multiple features from the get go. In this case we’re just exposing the twitter widget feature.

Lastly, we need a placement.info file to tell orchard where to put the various shapes we’ve defined in our driver. For more information about placement.info, check out the docs.

<Placement>
    <Place Parts_TwitterWidget="Content:1"/>
    <Place Parts_TwitterWidget_Edit="Content:7.5"/>
</Placement>

Creating the views
When we display our twitter widget, Orchard will look for the template that matches the shape being rendered and it will look in a variety of places, including in the module it is declared in and ultimately in the active theme. This allows module developers to create widgets and functionality with a set of default templates that theme authors can then easily override.

In our case, we defined the shape “Parts_TwitterWidget” when displaying and our driver told orchard to use Parts/TwitterWidget.cshtml for the editor form. As such, create two new razor templates in your module - Views/Parts/TwitterWidget.cshtml and Views/EditorTemplates/Parts/TwitterWidget.cshtml as follows;

Views/Parts/TwitterWidget.cshtml

@using SampleSiteModule.Models

<ul>
    @foreach (Tweet tweet in Model.Tweets)
    {
        <li>@tweet.DateStamp<br/>@tweet.Text<br/>@tweet.FriendlyDate</li>
    }
</ul>

Views/EditorTemplates/Parts/TwitterWidget.cshtml

@model SampleSiteModule.Models.TwitterWidgetPart

<fieldset>
    <legend>Latest Twitter</legend>
    <div class="editor-label">@T("Twitter username"):</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.TwitterUserName)
        @Html.ValidationMessageFor(m => m.TwitterUserName)
    </div>
    <div class="editor-label">@T("Number of tweets"):</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.MaxPosts)
        @Html.ValidationMessageFor(m => m.MaxPosts)
    </div>
    <div class="editor-label">@T("Cache (minutes)"):</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.CacheMinutes)
        @Html.ValidationMessageFor(m => m.CacheMinutes)
    </div>
</fieldset>

Go forth and try it out
If you open orchard now and activate the “Twitter Widget” feature, you can add it to a zone for the homepage layer and you should get;

image

Hopefully that wasn’t too painful. I’m trying to keep the posts as concise as I can yet cover off the main detail.

18 comments:

  1. I think you'll want to check out DateTimeShapes in Orchard.Core.Shapes. You should be able to Display(New.DateTimeRelative(dateTimeUtc: someDate))

    ReplyDelete
  2. Thanks for posting this series. I'm about to build a site for a friend (and also chose Orchard as it looks like it has the makings of a great CMS and I'm familiar with MVC3/Razor) however I've found the Orchard Doc's don't really go into detail about how a developer should build sites. What projects to create, etc. I'm sure I would have worked it out but this series is perfectly timed.

    Thank You

    ReplyDelete
  3. @Bertrand, thanks will check it out.

    @Anonymous, thanks for the kind words, hoping to have more in the series soon - maybe another post today on shape alternates and tweaking layout....

    ReplyDelete
  4. This is a great series - well written and easy to follow along. I'm just starting to take a look at Orchard and this was a good intro for me to get familiar with building some of the components.

    ReplyDelete
  5. It's an amazing serie! I'm stepping up with you and read any external links. Thanks for this amazing article :)

    ReplyDelete
  6. These articles and the accompanying code are filling the gaps I've had after reading the documentation (which is quite good, I'm a little slow :P ). Thanks again!

    ReplyDelete
  7. This is great, at last I can grasp where we are going!

    ReplyDelete
  8. Lovely post, very helpful! I'm running in to one problem:
    in TwitterWidgetRecordHandler, line 16

    Filters.Add(StorageFilter.For(repository));

    I get this error: The type 'SampleSiteModule.Models.TwitterWidgetRecord' cannot be used as type parameter 'TRecord' in the generic type or method 'Orchard.ContentManagement.Handlers.StorageFilter.For(Orchard.Data.IRepository)'. There is no implicit reference conversion from 'SampleSiteModule.Models.TwitterWidgetRecord' to 'Orchard.ContentManagement.Records.ContentPartRecord'.

    I'm running Orchard 1.2.41

    ReplyDelete
  9. Disregard my last comment please, I just found the solution (forgot to inherit ContentPartRecord in the TwitterWidgetRecord class).

    ReplyDelete
  10. Hi Thanks for the article on the ITwitter Service/Widget. I am building a similar Module that Queries custom datatables, inside the Orchard Database, thgese tables are already populated with data. I want to follow the same path as you did with twitter, but instead of querying twitter, I just want to query existing custom databases using select statments and such. Any advice you can give me on how to accomplish this? I am new to MVC and nHibernate. I can't figure out the data access side of things.
    Thanks
    Oceantrain

    ReplyDelete
  11. Hi, I enabled the feature but I can't add it to a zone as it doesn't appear in the list...

    ReplyDelete
    Replies
    1. Looks like you may have missed a step or two out - if you give more details I might be able to help.

      Delete
    2. Hi, it seems to have fixed itself. Would there be a caching issue? Also, another question, how can we cache the configuration of the widgets so that it doesn't make subsequent call to OrchardCMS database?

      Delete
  12. Hi, I have enabled the feature on Orchard 1.3.10 and received this error:
    Server Error in '/' Application.

    None of the constructors found with policy 'Orchard.Environment.AutofacUtil.DynamicProxy2.ConstructorFinderWrapper' on type 'SampleSiteModule.Models.TwitterWidgetDriver' can be invoked with the available services and parameters:
    Constructor 'Void .ctor(SampleSiteModule.Services.ITwitterService)' parameter resolution failed at parameter 'SampleSiteModule.Services.ITwitterService twitter'.

    Please give me some advices,
    Thanks,

    ReplyDelete
  13. This is really helpful. Thank you so much for the code. I really wanted to create a widget like this on my site to track recent tweets. Keep sharing!

    ReplyDelete
  14. In my case, I dont understand why the function Create in Migration.cs is never called so the table cant be created in base !
    Is someone got the same problem ? I tried to put the UpdateFrom... function but no success at all :( perhaps i dont put the right number of version...

    Thanks if someone can help me !

    Nico

    ReplyDelete
  15. Very good article. I take several days to learn to many places some points about Orchard. And your sample clarify many points. Effectively when you don't work on Orchard during the entire but sometime for Research&Development you need to have documentation with clear view. So really thank you.

    ReplyDelete
  16. Hi, I have just enabled the feature TwitterWidget on Orchard 1.7.10 and received this error:
    Server Error in '/' Application.

    None of the constructors found with 'Orchard.Environment.AutofacUtil.DynamicProxy2.ConstructorFinderWrapper' on type 'SampleSiteModule.Models.TwitterWidgetDriver' can be invoked with the available services and parameters:
    Cannot resolve parameter 'SampleSiteModule.Services.ITwitterService twitter' of constructor 'Void .ctor(SampleSiteModule.Services.ITwitterService)'.

    Please give me some advices,
    Thanks,

    ReplyDelete