Custom Javascript Banner Rotator – Part 2: jQuery!

As I suggested in part 1 of this series, I recently discovered the wonderful world of jQuery, the “write less, do more javascript library”. And sure enough, this is one tool that really lives up to its name! Not only can you do all sorts of neat (even useful!) javascript effects and events, but they are suprisingly easy to do! I had put learning this on the backburner a while back, but after publishing my Banner Rotator, I figured it was about time to bring that sucker front and center and start cooking!

The banner rotator script I posted yesterday presented the perfect opportunity to jump in. Using this incredible framework, not only was I able to wrap the functionality into a plugin, but I was able to improve and refactor it to make it reusable (which is a good thing because I’m going to be using it on the city’s Convention Center website). It turned out to be so easy, I don’t even have to waste time with a back story. Let’s go straight to the code!

First thing you want to do when making a jQuery plugin is create the function and assign it to jQuery. I’ve also included the (improved!) local variables from last time to keep track of where we are in the rotation.

.

var imgSrcs = [];
var curSrc = 0;

var imgElems;
var curElem = 0;

var fadeSpeed;
var imageTimeout;

function img(src, alt){
this.src = src;
this.alt = alt;
}

jQuery.fn.bannerswapper = function()
{};

Notice that I’ve created a class img to hold the image data, including source and alternate text attributes.

Now we want to provide a way for users to supply arguments for imageTimeout and fadeSpeed. In addition, this time, we’re going to be reading from an XML file which contains the list of images (along with their alt tags!) to build our list. Handling paramters is pretty straightforward:

jQuery.fn.bannerswapper = function(xmlFile, settings)
{
var banner = this;
settings = jQuery.extend({
fade: 6000,
timeout: 10000
}, settings);
}
fadeSpeed = settings.fade;
imageTimeout = settings.timeout;

the jQuery.extend allows us to declare the settings variables, including default values. In this case, fade is set to 6000ms, and timeout (time between frames) is 10000. If users supply parameters (as we’ll see later) then these default values will be replaced. Also note that because xmlFile is explicitly defined in the function signature, it is now a REQUIRED variable.

Next, we want to read the xml file, and create our array of elements. I cannot emphasize in words just how easy this is to do with jQuery, so I’ll let the code speak for itself:

// load image sources
$.get(xmlFile, function(xml)
{
var i = 0;
$(xml).find('image').each(function()
{
imgSrcs[i++] = new img($(this).attr('src'), $(this).attr('alt'));
});

// make sure there are at least 2 elements
if (imgSrcs.length < 2) return;

// only create element if it's not already there
if (banner.length == 1) banner.append(document.createElement('img'));

// get array of image elements to swap
imgElems = $('img', banner);

// start toggling!
toggle();
});

That is literally ALL there is to it! I create a new instance of the img class for each element in the xml file and append it to the array. Once the array is complete, we just call toggle and we’re on our way!

One thing I did discover was that my previous example did not handle situations where there are an ODD number of images. So below is an improved method which toggles the image back and forth, taking this into account:

function toggle()
{
// move to next image
if (++curSrc >= imgSrcs.length) curSrc = 0;

// grab ref to elements for updating
var frontImg = imgElems[curElem];
if (++curElem > 1) curElem = 0;
var backImg = imgElems[curElem];

// set current image to hide next
frontImg.className = "";
frontImg.removeAttribute('style');

// prepare to swap image
backImg.className = "show";
backImg.src = imgSrcs[curSrc].src;
backImg.alt = imgSrcs[curSrc].alt;

// fade in next image and repeat
setTimeout(function() { $('.show').fadeIn(fadeSpeed, toggle); }, imageTimeout);
}

Can you believe it’s really that easy? Now all we have to do is call our plugin, here is the syntax.

<asp:ScriptManagerProxy ID="ScriptManager" runat="server">
<
Scripts>
<
asp:ScriptReference Path="/files/scripts/jquery/jquery-1.2.6.pack.js" />
<
asp:ScriptReference Path="/files/scripts/jquery/bannerswapper/jquery.BannerSwapper.js" />
</
Scripts>
</
asp:ScriptManagerProxy>
<script type="text/javascript">
$().ready(function()
{
$('#banner_container').bannerswapper('/files/scripts/jquery/images.xml');
});
</script>
<
div id="banner_container">
<
img src="/images/banners/banner10.jpg" alt="" />
</
div>

Also note that I’ve moved all of the stylings to an external sheet so that needs to be included in your page header as well.

I’ve packed all the files I used (excluding the images) into a zip which I’ve uploaded to myBloop. If you’d like to take a look at the full source, please use the link below. Your comments are appreciated!

Download Banner Swapper jQuery Plugin

AJAX News Rotator in Sitefinity Part 1

I’ve recently been developing a control to display recent news in a rotating fashion. Originally, I was using the RadRotator control from Telerik. This is a powerful control that takes a lot of the guesswork out of displaying and rotating news. However, I decided to build my own for two reasons.

First, I didn’t like the way the rotator control loads ALL of the news items before running. The RadRotator works by preloading all of the elements, placing each item into its own div element, then cycling through each, toggling visibility of each one at a time. This solution works for a few elements, but if you’re cyclying through a half dozen or more items, each with an image, it can be an expensive procedure, especially since the control has to finish loading in order for the page to run. This leaves users viewing half a screen while the content loads.

The other reason I decided to build my own was just out of sheer curiousity and a desire to learn, especially since this is a perfect candidate to get my feet wet in the Ajax world that I’ve been dying to dive into.

Anyway, enough back-story, let’s get to the code! The first thing I had to do was retrieve the news items that will be displayed. I’ll be storing the Guids for each news item in the ViewState for quick, on-demand retrieval. The Guids go in a Generic List, which will be accessed by a CurrentItem index, which checks to make sure that this value is always within the valid range of elements.

protected List newsIDs  
{  
    get {  
        if (ViewState["NewsIDs"] == null) ViewState["NewsIDs"] = new List();  
        return (List)ViewState["NewsIDs"];  
    }  
    set { ViewState["NewsIDs"] = value; }  
}  
 
protected int CurrentItem  
{  
    get {  
        if (ViewState["CurrentItem"] == null) ViewState["CurrentItem"] = 0;  
        return (int)ViewState["CurrentItem"];  
    }  
    set {  
        if (value > newsIDs.Count - 1)  
            ViewState["CurrentItem"] = 0;  
        else if (value < 0)  
            ViewState["CurrentItem"] = newsIDs.Count - 1;  
        else ViewState["CurrentItem"] = value;  
    }  
} 

Since I’m using Sitefinity (my content management system of choice!), I’ll need to use the NewsManager to retrieve these items. Because of the nature of the News API, it is necessary for me to retrieve the full list of news items on Page_Load. Ideally, I would only be grabbing the Guid for each item, but since the only way to retrieve them is to retrieve the full items, I’ll have to do that. Fortunately, this is only in the code behind, and isn’t rendering to the page so the overhead should be minimal, especially since it’s only done on the initial Page_Load

protected void Page_Load(object sender, EventArgs e)  
{  
    if (this.manager == null) this.manager = new NewsManager("News");  
      
    if (!IsPostBack)  
    {  
        BindData();  
    }  
}  
 
private void BindData()  
{  
    IMetaSearchInfo[] filters = new IMetaSearchInfo[3];  
    filters[0] = new MetaSearchInfo(MetaValueTypes.DateTime, "Expiration_Date", DateTime.Now, SearchCondition.GreaterOrEqual);  
    filters[1] = new MetaSearchInfo(MetaValueTypes.ShortText, "Category", "Top Story");  
    filters[2] = new MetaSearchInfo(MetaValueTypes.DateTime, "Publication_Date", DateTime.Now, SearchCondition.LessOrEqual);  
 
    IList newsList;  
    newsList = manager.Content.GetContent("Publication_Date DESC", filters);  
 
    newsNavRepeater.DataSource = newsList;  
    newsNavRepeater.DataBind();  
 
    foreach (IContent content in newsList)  
        newsIDs.Add(content.ID);  
 
    // start with the first news Item UpdateNews((IContent)newsList[0]);  
} 

The newsNavRepeater is simply a repeater of LinkButtons that will tie to the individual news item ids.

<div class="sf_newsRotator"> <telerik:radajaxpanel id="NewsAjaxPanel" runat="server" width="550" height="185" loadingpanelid="NewsLoadingPanel"> <div id="newsFrame" class="newsFrame"> <h3><asp:HyperLink ID="lnkTitle" runat="server" /></h3> <asp:HyperLink ID="lnkFullStory" runat="server" CssClass="fullstory" Text="Full Story &raquo;" /> <div class="sf_newsImage"> <asp:HyperLink ID="lnkThumbnail" runat="server" /> </div> <p><strong style="font-size: .9em;">Posted  
                <asp:Label ID="PublicationLabel" runat="server" /></strong><br /> <asp:Literal ID="SummaryLabel" runat="server" /> </p> </div> <div class="newsNav"> <asp:Repeater ID="newsNavRepeater" runat="server" OnItemDataBound="newsNavRepeater_ItemDataBound" OnItemCommand="newsNavRepeater_ItemCommand"> <ItemTemplate> <asp:LinkButton ID="lnkItem" runat="server" /> </ItemTemplate> </asp:Repeater> </div> </telerik:radajaxpanel> <a class="allnews" href="/news/default.aspx" title="View All News from the City of McAllen"> All News</a> <a href="/news.rss" class="rss" target="_blank" title="Subscribe to the City of McAllen Latest News RSS Feed"> <img src="/images/icons/icon-rss.gif" alt="RSS Feed" style="vertical-align: middle;" /></a></div><telerik:radajaxloadingpanel id="NewsLoadingPanel" runat="server" height="190px" width="550px" transparency="50" enableviewstate="false"> <div style="background: #ccc; height: 100%"> <img style="margin-top: 75px; z-index: 1" alt="Loading..." src='<%= RadAjaxLoadingPanel.GetWebResourceUrl(Page, "Telerik.Web.UI.Skins.Default.Ajax.loading.gif") %>' style="border: 0px;" /> </div> </telerik:radajaxloadingpanel>

The buttons are populated using the OnItemBound event to fill each link with the appropriate post information from the bound IContent DataItem.

protected void newsNavRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    // get link LinkButton lnk = e.Item.FindControl("lnkItem") as LinkButton;

    // get dataitem IContent newsItem = e.Item.DataItem as IContent;
    int itemIndex = e.Item.ItemIndex + 1;
    lnk.Text = itemIndex.ToString();
    lnk.CommandArgument = (itemIndex - 1).ToString();
    lnk.ToolTip = newsItem.GetMetaData("Title").ToString();
} 

Once the navigation is bound, we need to bind the first news item, which is the first item in the newsList collection above (newsList[0]). Note how this item has to be cast as IContent and passed into the UpdateNews method.

private void UpdateNews(IContent newsItem)  
{  
    // make sure item still exists if (newsItem == null)  
    {  
        // if not, reset data! BindData();  
        return;  
    }  
 
    // bind current item data string newsTitle = newsItem.GetMetaData("Title").ToString();  
    lnkTitle.Text = newsTitle;  
    lnkTitle.NavigateUrl = string.Format("/news/default{0}.aspx", newsItem.Url.ToLower());  
    lnkThumbnail.ImageUrl = newsItem.GetMetaData("Thumbnail").ToString();  
    lnkThumbnail.ToolTip = newsTitle;  
    lnkThumbnail.NavigateUrl = lnkTitle.NavigateUrl;  
    lnkThumbnail.Text = newsTitle;  
    SummaryLabel.Text = newsItem.GetMetaData("Summary").ToString();  
    PublicationLabel.Text = ((DateTime)newsItem.GetMetaData("Publication_Date")).ToString("dddd MMMM dd, yyyy");  
 
    LinkButton selectedLink = newsNavRepeater.Controls[CurrentItem].FindControl("lnkItem") as LinkButton;  
    selectedLink.CssClass = "selected";  
} 

Notice also that this method ensures that a valid item was found, if not (such as a newsitem deleted or new items posted) it will reset, binding all new data and start all over again. Also, at the bottom, we since the Controls array of the newsNavRepeater has as many elements as the newsList, we can grab use that index to select the associated LinkButton and set its CSS class so that we can highlight the selected item!

All that is left to do is handle the OnItemCommand event of the newsNavRepeater (since these buttons are what trigger a different item). Remember that the argument for each LinkButton set in BindData is the index of that news item’s Guid in the newsList Collection. So all we have to do is retrieve that one news item (using the News API) and run the UpdateNews method again! Notice how before we update the CurrentItem with the new value, we use it to “unselect” the previous news item.

protected void newsNavRepeater_ItemCommand(object source, RepeaterCommandEventArgs e)  
{  
    // unselect css before changing LinkButton selectedLink = newsNavRepeater.Controls[CurrentItem].FindControl("lnkItem") as LinkButton;  
    selectedLink.CssClass = "";  
 
    CurrentItem = Int32.Parse(e.CommandArgument.ToString());  
    IContent newsItem = manager.Content.GetContent(newsIDs[CurrentItem]);  
    UpdateNews(newsItem);  
} 

And that’s all there is to it! Wrap all of this inside of a RadAjaxPanel and you’ve got yourself an interactive news roll! There’s still a few things to add, namely a Timer control to automatically cycle the news, but I will cover that my next entry. I hope this has been helpful, and as always your comments are welcome and appreciated!