As seen on Tech Forum: The Twitter channel gadget
Inspired by Twingly channels me, Mats Hellström and Rachel Goldthorpe decided to create a Twitter channel gadget for EPiServer.
The project consist of two parts:
- A scheduled job that imports tweets with a specific keyword into a dynamic data store
- A gadget that shows the imported tweets and lets the editors comment specific tweets (for other editors to read) as well as search among tweets and their comments
What is this good for? Well, you get to comment what’s being said about for example your brand on Twitter but you only need to share the comments with your loved ones.
Instead of 2000 more words, here are two screenshots:
The search result listing is of course updated as you type (to stress test the dynamic data store) and after lots of usability studies we decided that comments should be added by clicking the profile image. When finished typing, hit tab to save the comment.
Now we’re just pulling tweets from Twitter, but his could easily be extended to grab data from other services as well. Think big, think YQL,
The tweet importer
The tweet importer uses the Twitter search API which return Json objects. These objects are serialized into C# objects using the DataContractJsonSerializer that are later pushed into the dynamic data store. Note that we’re using the DataMember attribute to tell which properties the Json serializer should consider, but the attribute EPiServerDataMember to tell which properties that should be saved into the dynamic data store.
[DataContract]
[EPiServerDataContract]
public class TwitterItem
{
[DataMember(Name = "text", Order = 0)]
[EPiServerDataMember]
public string Text { get; set; }
[DataMember(Name = "to_user_id", Order = 1)]
public int? ToUserId { get; set; }
[DataMember(Name = "from_user", Order = 2)]
[EPiServerDataMember]
public string FromUser { get; set; }
[DataMember(Name = "id", Order = 3)]
[EPiServerDataMember]
public long TweetId { get; set; }
[DataMember(Name = "from_user_id", Order = 4)]
public int? FromUserId { get; set; }
[DataMember(Name = "iso_language_code", Order = 5)]
public string IsoLanguageCode { get; set; }
[DataMember(Name = "profile_image_url", Order = 6)]
[EPiServerDataMember]
public string ProfileImageUrl { get; set; }
[DataMember(Name = "created_at", Order = 7)]
public string CreatedAt { get; set; }
[EPiServerDataMember]
public DateTime StatusDate { get; set; }
}
Each TwitterItem is embedded in a ChannelItem, which will be the type our dynamic data store will contain . Remember that the dynamic data store can contain one and only one type. The ChannelItem contains the tweet, its comments and a last updated property. Since we want all public properties to go into the dynamic data store we don’t need to use the EPiServerDataContract attribute.
public class ChannelItem
{
public ChannelItem()
{
Comments = new List<String>();
}
public ChannelItem(TwitterItem tweet)
{
Comments = new List<String>();
Tweet = tweet;
LastUpdated = tweet.StatusDate;
}
public TwitterItem Tweet { get; set; }
public System.DateTime LastUpdated { get; set; }
public List<String> Comments { get; set; }
}
We’re using the TweetId to make sure that we don’t import duplicates of a tweet, meaning that we can skip to implement the IDynamicData interface and its Id property which can be used if you’d like to assign Guid:s to your objects.
private static int AddTweetsToDynamicDataStore(List<TwitterItem> tweets)
{
DynamicDataStore<ChannelItem> channelStore =
DynamicDataStore<ChannelItem>.CreateStore(ChannelStoreName,
true);
int importCount = 0;
foreach (var item in tweets)
{
var currentItems = (from c in channelStore where
c.Tweet.TweetId == item.TweetId
select c).ToList();
if (currentItems.Count > 0)
{
continue;
}
ChannelItem cItem = new ChannelItem(item);
channelStore.Save(cItem);
importCount++;
}
return importCount;
}
The gadget
The scheduled job makes sure that the Twitter channel is updated regularly with beautiful tweets on a particular subject. It’s time to show the tweets in a gadget and let editors search and make comments. It’s Mvc time, this beautiful little framework that EPiServer SiteCenter is built upon.
Let’s start by looking at the different Views we’ve got:
Tweets
Takes a list of ChannelItems and renders the tweets and their comments (using the Comments View).
Comments
Takes a ChannelItem and renders its comments.
Index
Takes a list of ChannelItems. Renders a search box and then passes the ChannelItems to the Tweets View.
With these Views we just need something that receives requests from the gadget, modifies and/or collects some data in the dynamic data store and then passes it to the right view. We’re talking about at the heart in out twitter gadget, the Controller. These are the main Actions in our Controller:
Index
Responsible for fetching all channel items and passing them to the Index View.
AddComment
Responsible for adding an incoming comment to a tweet and passing the resulting ChannelItem to the Comment View.
public ActionResult AddComment(string comment, string tweetId)
{
ChannelItem item = (from c in channelStore
where c.Tweet.TweetId == long.Parse(tweetId)
select c).ToList()[0];
// Add time and user to comment and modify last updated date on tweet
item.LastUpdated = DateTime.Now;
item.Comments.Add(string.Format("{0} [by {1} on {2}]",
comment, User.Identity.Name, item.LastUpdated.ToTwitterDate()));
channelStore.Save(item);
// Return all comments for tweet
return View("Comments", item);
}
GetFilteredTweets
Responsible for fetching all ChannelItems that contains a specific keyword and then passing them to the Tweets View. Pay attention to how we’re using Count to search for a string in the comments collection.
public ActionResult GetFilteredTweets(Guid gadgetId, string searchTerm)
{
var items = (from c in channelStore
where
c.Tweet.Text.Contains(searchTerm) ||
c.Tweet.FromUser.Contains(searchTerm) ||
c.Comments.Count(s => s.Contains(searchTerm)) > 0
orderby c.LastUpdated descending
select c).ToList();
return View("Tweets", items);
}
Now we just need to add an event handler to the search box (fire off a request to the GetFilteredTweets Action whenever a user starts typing) as well as an event handler when adding a comment (send comment to AddComment Action). Looking at the code below you’ll see that we’re using the ajax method inside the gadgetContext. This method takes care of sending the gadget id so we don’t need to worry about that.
twitterChannel.init = function(e, gadgetContext) {
twitterChannel.gadgetInstance = gadgetContext;
$("#searchfield").keyup(function() {
twitterChannel.filterTweets($(this).val());
});
twitterChannel.addCommentClickHandler();
};
twitterChannel.filterTweets = function(searchTerm) {
twitterChannel.gadgetInstance.ajax({
type: "GET",
url: twitterChannel.gadgetInstance.getActionPath(
{ action: "GetFilteredTweets" }),
data: "searchTerm=" + searchTerm,
contentType: "application/json; charset=utf-8",
dataType: "html",
success: function(result) {
$("#twitterchannel").html(result);
twitterChannel.addCommentClickHandler();
}
});
}
twitterChannel.addComment = function(target, tweetId, comment) {
twitterChannel.gadgetInstance.ajax({
type: "POST",
url: twitterChannel.gadgetInstance.getActionPath({ action: "AddComment" }),
data: { tweetId: tweetId, comment: comment },
dataType: "html",
success: function(result) {
$(target).parent().find(".edit").addClass("hidden");
$(target).html(result);
}
});
}
Whenever a tweet is commented its last updated property is updated, making newly commented tweets appear on top.
The gadget also has a setting where the editor can select how many tweets to show. This is handled by the Configure View and the Configure and SaveSettings Actions. The settings are stored in, you guessed it, the dynamic data store.
Happy tweet commenting!
Download
Don’t forget to also grab the Scheduled Jobs Monitor gadget by Björn Olsson.
Nice work!
/ Pelle
The download now includes the Css file as well...