Rotating Announcements with a Data View Web Part and jQuery

Oftentimes people go searching for plugins and already-written code to accomplish their goals. I find that it often gets people into more trouble than it’s worth as they usually don’t understand what they have deployed and it doesn’t work well in their specific situation. Of course, here I am posting something that those of you reading can use in exactly that way. My advice is to always look at these posts as *examples* of how you might do something. Make sure you understand what you get and how you need it to work on your end.

In this particular case, the search team wanted to show one random item from an Announcements list on the Search Results page and allow the users to cycle through all of the other available (unexpired) items. Each item will explain something new about what’s going on with search enhancements or give a tip on how to search more effectively.

The natural inclination, like I mention above, might be to go and look for a “rotating news item” jQuery plugin. But it was far easier to use a Data View Web Part (DVWP) along with a very small amount of script to make it happen. This is a method that I like to use often: have the DVWP “paint” the content onto the screen and then use jQuery to add some behavior to it. That way, you’re having the server do the heavy lifting, but also enhancing the user experience with the script. I also could have done this entirely with client side script using SPServices, but it doesn’t make sense to do so if you can keep the appropriate amount of processing on the server.

Rotating Announcements

In my particular case, the Announcments list that I wanted to use as the DataSource was in a different Site Collection. No worries, I just used the Lists Web Service as the DataSource in the DVWP. What you see below is just the DataSource section of my DVWP. Those of you who have used the Lists Web Service before, whether with SPServices or directly, will recognize the SOAP request and all its trappings. When you set up a SOAP DataSource in SharePoint Designer (SPD), this is what SPD creates for you. (I’ve cleaned up the XML that SPD generates to make it easier to read, including removing the escaping in the parameter values.)

<DataSources>
  <SharePoint:SoapDataSource runat="server" SelectUrl="http://[full path]/_vti_bin/lists.asmx" SelectAction="http://schemas.microsoft.com/sharepoint/soap/GetListItems" SelectPort="ListsSoap" SelectServiceName="Lists" AuthType="None" WsdlPath="http://[full path]//_vti_bin/lists.asmx?WSDL" XPath="" ID="SoapDataSource1">
    <SelectCommand>
      <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <soap:Body>
          <GetListItems xmlns="http://schemas.microsoft.com/sharepoint/soap/">
            <listName>Announcements</listName>
            <query><Query><OrderBy><FieldRef Name="Created Date" Ascending="FALSE"><OrderBy><GroupBy><GroupBy><Where><Or><Gt><FieldRef Name="Expires"><Value Type="DateTime"><Today><Value><Gt><IsNull><FieldRef Name="Expires"><IsNull><Or><Where><Query></query>
            <viewFields><ViewFields><FieldRef Name="Title"><FieldRef Name="Abstract"><ViewFields></viewFields>
            <rowLimit>0</rowLimit>
          </GetListItems>
        </soap:Body>
      </soap:Envelope>
    </SelectCommand>
    <InsertCommand>
    </InsertCommand>
    <UpdateCommand>
    </UpdateCommand>
    <DeleteCommand>
    </DeleteCommand>
  </SharePoint:SoapDataSource>
</DataSources>

You’ll note that I’ve added my own filtering for the Expires column, since we don’t get that “for free” like we do with a standard Announcements list view. We also have added an Abstract column to the Announcements list, since the Body of each Announcement tends to be pretty long. Showing a few short sentences is enough to give the users the gist of what the post is about. If they want to read more, they can click on the Title, which takes them to the full Annoucement. I’ve also requested only the columns that I need to try to reduce the number of bits through the wire. In this instance, the volume will never be that large, but it can’t hurt.

Next comes the XSL in the DVWP. What we wanted on the page was fairly simple, so the XSL is pretty straightforward. That said, it’s entirely custom: SPD has no idea how to do things like this, so it’s a coding exercise.

  <xsl:output method="html" indent="no"/>
  <xsl:param name="URL"/>
  <xsl:param name="SERVER_NAME"/>

  <xsl:template match="/" xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:d="http://schemas.microsoft.com/sharepoint/dsp" xmlns:asp="http://schemas.microsoft.com/ASPNET/20" xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer" xmlns:SharePoint="Microsoft.SharePoint.WebControls" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ddw1="http://schemas.microsoft.com/sharepoint/soap/" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
    <xsl:call-template name="dvt_1"/>
  </xsl:template>

  <xsl:template name="dvt_1">
    <xsl:variable name="Rows" select="/soap:Envelope/soap:Body/ddw1:GetListItemsResponse/ddw1:GetListItemsResult/ddw1:listitems/rs:data/z:row"/>
    <xsl:variable name="TotalItems" select="count($Rows)"/>
    <xsl:variable name="ShowItem" select="ddwrt:Random(1, $TotalItems)"/>
    <div class="search-news">
      <xsl:for-each select="$Rows">
        <xsl:call-template name="dvt_1.rowview">
          <xsl:with-param name="TotalItems" select="TotalItems"/>
          <xsl:with-param name="ShowItem" select="$ShowItem"/>
        </xsl:call-template>
      </xsl:for-each>
    </div>
    <div class="search-news-footer">
      <span class="search-news-prev"><a onclick="showPrevNewsItem();">&lt; Previous</a></span>
      <span class="search-news-counts"><span id="search-news-active-item"><xsl:value-of select="$ShowItem"/></span> of <xsl:value-of select="$TotalItems"/></span>
      <span class="search-news-next"><a onclick="showNextNewsItem();">Next &gt;</a></span>
    </div>
  </xsl:template>

  <xsl:template name="dvt_1.rowview">
    <xsl:param name="ShowItem"/>
    <div class="search-news-item">
      <xsl:attribute name="style">
        <xsl:if test="position() != $ShowItem">display:none;</xsl:if>
      </xsl:attribute>
      <div class="search-news-title">
        <a target="_blank" href="http://[full path]/Lists/Announcements/DispForm.aspx?ID={@ows_ID}&amp;Source={concat('http://', $SERVER_NAME, $URL)}">
          <h3><xsl:value-of select="@ows_Title"/></h3>
        </a>
        <xsl:value-of select="ddwrt:FormatDate(string(@ows_Created), 1033, 1)"/>
      </div>
      <div class="search-news-abstract">
        <xsl:value-of select="@ows_Abstract" disable-output-escaping="yes"/>
      </div>
    </div>
  </xsl:template>

Notice that I’m emitting nice DIVs rather than SharePoint’s dreaded tables. Say what you will about which is better, but when you use a DVWP, the markup is entirely up to you.

There are several interesting bits in this XSL. First, becasue we wanted to display a random item on every page load, I needed a way to come up with which item to display. In lines 16 and 17, I calculate how many items are available and generate a random number between 1 and that number of items. The value of the ShowItem variable drives which item is visible on page load. In fact, all of the items are loaded in the page, but only that one item is visible. This sets things up for the capability to cycle through all of the current news items.

Lines 22-24 build the “footer” of the DVWP. In the footer, there are Previous and Next links, as well as a counter which displays which items the user is currently viewing and the total number of items. Since the number of items will be variable, and we hope that users will take the time to cycle through them, we wanted to give them a feel for how many items are available to them. If you look at the Previous link, you’ll see that onclick I’ve added a call to showPrevNewsItem();. Next has a call to showNextNewsItem();. Here’s what that script looks like:

function showPrevNewsItem() {
  var newsItems = $("div.search-news-item");
  var newsItem = $("div.search-news-item:visible");
  newsItem.each(function() {
    $(this).hide();
    if($(this).index() > 0) {
      $(this).prev().show();
    } else {
      newsItems.last().show();
    }
  });
  var activeItem = $("div.search-news-item:visible").index() + 1;
  $("#search-news-active-item").html(activeItem);
}

function showNextNewsItem() {
  var newsItems = $("div.search-news-item");
  var newsItem = $("div.search-news-item:visible");
  newsItem.each(function() {
    $(this).hide();
    if($(this).index() < (newsItems.length - 1)) {
      $(this).next().show();
    } else {
      newsItems.first().show();
    }
  });
  var activeItem = $("div.search-news-item:visible").index() + 1;
  $("#search-news-active-item").html(activeItem);
}

In each function (yes, I could probably combine them into one function that takes a parameter for which way to move, but I find it easier to think through this way), I find the currently visible news item, hide it, and then show the previous or next one. If we’ve reached the “top” or the “bottom” of the items, I wrap around to the end or the beginning. This allows the user to cycle through the items in a continuous loop if they choose to. The last thing I do in each function is figure out which item we’re displaying and update the active item number in the footer of the DVWP.

The last piece is the CSS to make it all look pretty.

/* Search News */
div.search-help {
  padding:5px;
  height:20px;
  background-color:#FFFFD9;
  text-align:center;
  vertical-align:middle;
}
div.search-help a, div.search-help a:visited {
  font-family:"News Gothic MT", verdana, sanserif;
  font-size:12px;
  color:#E04805;
}
div.search-news-container {
  margin-top:10px;
  margin-bottom:10px;
  border:2px #f5f3f0 solid;
  border-radius:15px 15px 10px 10px;
  -moz-border-radius:15px 15px 10px 10px;
}
div.search-news-container .ms-WPHeaderTd {
  background-color:#f5f3f0;
  border-radius:10px 10px 0 0;
  -moz-border-radius:10px 10px 0 0;
  border:0;
  padding-left:15px;
}
div.search-news-container .ms-wpTdSpace, div.search-news-container .ms-WPHeaderTdSelection {
  display:none;
}
div.search-news {
  min-height:75px;
}
.search-news-prev, .search-news-next {
  cursor:pointer;
}
.search-news-prev a:hover, .search-news-next a:hover {
  color:#E04805;
}
.search-news-prev {
  float:left;
}
.search-news-next {
  float:right;
}
.search-news-footer {
  margin-top:20px;
  text-align:center;
}
/* Search News */

As you can see in the screenshot of the end result above, I’m using some nice rounded corners for the header and the border around the whole thing. This is a CSS3 capability, and since we have a mixed browser environment, I have to cover all of the bases, including older versions of Mozilla (Firefox) and Chrome. The majority of the users have IE8, so they don’t get the rounded corner goodness yet (as you can see below), but they will all soon be getting IE9, at which point they will have prettiness, too.

Rotating Announcements in IE8

And there you go. A nice, simple little rotating announcements view. Users love this sort of thing, and it helps to get the news out there in a new and interesting way (at least for SharePoint).

Similar Posts

8 Comments

  1. Hello Marc,

    I’m looking forward to applying this technique but I don’t seem to have the Data View Web Part available. We currently use WSS 3.0.

    Thanks in advance.

    Brett Barkman

  2. Marc,

    I am trying to figure out a way to more dynamically define the SelectUrl property of a SharePoint DataSource pointing to Lists.asmx. The DataView webpart works properly on our production site, but when the systems group restores the content DB at our DR site the url for the site is just slightly different which causes several webparts to fail. Do you know any workarounds for this problem.

  3. Thank you for this post. it was somewhat helpful. But if you are trying to teach someone something this has several holes that users will get stumped on. You do not explain everything very clearly. I’m assuming you are a professor. Also, when people ask for help on your post such as Trent here, it is not helpful to Trent or anyone else on the thread when you reply to his question with such a reply oozing with distaste, or malice. I’m assuming Trent knows why his DR site’s URLs do not match. This is why he’s asking the question about a more dynamic solution.

    1. Josh:

      I’m sorry if you were offended in some way. It was a legitimate question. If the URLs in a DR site don’t match production, then no one will be able to access anything where they expect it to be. Since I never heard back from Trent, we’ll never know what was really going on.

      M.

  4. Our DR site is a hot site for critical applications. I am not involved in the architecture of our DR environment, but since it is hot and we are regularly backing up our site to DR they are using different different urls for each of the sites. Initially I was trying to tackle everything using DVWP’s, but since have started leveraging javascript as an alternative and have been able to resolve this issue because I can use relative urls to prevent issues I was having with the DVWP’s in our DR environment.

    1. Trent:

      That was where I was headed. In most cases, using relative URLs is the solution to using DVWP XSL in different farms. This is true whether you’re dealing with dev/staging/prod and the list or prod and DR.

      M.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.