You are on page 1of 9

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, Im 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;

Building a Hello world module Writing a widget

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;

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 ids and what nots 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 using using using System.ComponentModel; System.ComponentModel.DataAnnotations; Orchard.ContentManagement; 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 using using using System; System.Collections.Generic; Orchard.Environment.Extensions; 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, were 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 using using using using System; Orchard.ContentManagement; Orchard.ContentManagement.Drivers; Orchard.Environment.Extensions; 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 youve 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 using using using using using System.Data; Orchard.ContentManagement.MetaData; Orchard.Core.Contents.Extensions; Orchard.Data.Migration; Orchard.Environment.Extensions; 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 well 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 dont need to bother with this, but as were going to build on it, I set it up for multiple features from the get go. In this case were just exposing the twitter widget feature. Lastly, we need a placement.info file to tell orchard where to put the various shapes weve 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;

Hopefully that wasnt too painful. Im trying to keep the posts as concise as I can yet cover off the main detail.

You might also like