Middle Tier Magic: The SPSTCDC Web Site Speakers and Sessions Pages
If you missed the webinar, you can watch the recording here. As I mentioned during the webinar, all of the important code bits are shown below. If you have any questions, please feel free to post a comment.
</UPDATE>
I’m not going to make it to what may be the largest SharePoint rodeo outside of the Microsoft conference in October, but I did want to help out the good folks who are organizing it if I could. Since I can’t present or carry boxes for the inaugural SharePoint Saturday: The Conference, I thought I might be able to improve upon the Speakers and Sessions pages on the Web site a wee bit, so I offered. Michael Lotter and Dux (@meetdux) seemed happy to get some assistance, so away I went.
Another thing I thought I could do was offer up an explanation of how I put the pages together. They are built with solid SharePoint Middle Tier development, and looking at how they are built may be useful for people to understand. We didn’t have to deploy any code to the server and I did it all with SharePoint Designer.
In other words, here’s the first part of my “virtual SPSTC session”. I’ll also be doing a live, online session after SPSTC with Dux and Michael on Tuesday, August 16 at 2pm EDT [register]. In that session, we’ll talk in some depth about the inner working of the SPSTC site and the pages I built and am describing here. Mark your calendars for this free after-the-conference event!
Here’s a short video I put together talking about the free, live session on August 16th.
[jwplayer config=”Custom Player” mediaid=”14063″]
What’s a Conference?
In talking about the SPSTC site with Michael and Dux through email before I did anything, I thought it through like this.
In my mind, there’s a tripod which holds up any conference: speakers, sessions, and slots. (Cute that they all start with “s”, eh?) Each leg of the tripod can be related to 1-n of each of the other legs. So, a speaker can be doing 5 sessions, a session can have two speakers, a session can be offered three times, etc. (One outlier is a session which is offered at different times with different speakers. I’d usually think of that as a different session – it doesn’t happen often, anyway.)
Building a UI which allows the user to click hyperlinks in *any* of the directions is key to cover most of the use cases. So if I’m looking at a speaker, I should be able to click on his sessions as well as his slots. If I’m looking at a slot, I should be able to click on any of the speakers or sessions, etc.
You really need three main pages: one for each leg, each of which need to be sortable and filterable. Each of the pages should have slicing and dicing links at the top and they should be generated dynamically based on what the underlying data contains.
I’ve not seen a good example of this all working well and flexibly, as simple as it seems to be. I have no idea why, unless it’s that conference organizers get too caught up in the details of everything and can’t think in a database-y way.
The Speakers and Sessions Lists
There are two fairly simple lists which contain all the Speaker and Sessions info. They are inventively called: Speakers and Sessions. The important columns for each list are shown below, with Sessions on the right and Speakers on the left.
The two lists are related by the Speaker column in the Sessions list; it’s a Lookup column to the Title in the Speakers list and it allows multiple values for those sessions which have, well, more than one speaker.
The Speakers Page
The simpler of the two pages is the Speakers page. It simply lists all of the speakers in alphabetical order by last name. There’s also link at the top for All which shows the total count of speakers. This link is useful if you arrive at the page to see one speaker and then would like to see all of them again.
All of the information you see about the speakers is rendered with one Data View Web Part (DVWP). The DVWP uses both the Speakers and Sessions lists in an AggregateDataSource (also known as a Linked Data Source). This allows me to pull the information about each speaker from the Speakers list and also all of the sessions which that speaker will be leading, whether as the primary speaker (simply the first one in each session) or as a co-presenter.
The XSL for the DVWP is where the Middle Tier magic comes in. It’s not horribly complex, but it’s not stuff that you can do simply with the DVWP’s settings. As with most of the DVWPs I build, I wrote most of the XSL by hand so that I could have total control over the XSL templates, both how they worked and how they interrelated.
I won’t go through the XSL line by line here, but let me give you an overview of the templates and how they are working together. Note that I’ve renamed the templates from the standard dvt_1 type names to names which ought to make more sense. Good coding guidelines apply to any language you’re working with!
- Speakers – The Speakers template grabs the items we need from the Speakers list, renders the “All” link at the top of the page with the count of the speakers, sets up the table to contain them, and calls Speakers.rowview.
- Speakers.rowview – This template renders the information about the speakers. It looks more complicated than it actually is because there’s a lot of conditional logic so that we don’t display column labels where there aren’t values.
- Sessions – Here we go and gather the sessions for each particular speaker so that we can list them out below the speaker’s biographical information.
- Session.rowview – And finally, this template renders the link to each of the sessions.
<xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:d="http://schemas.microsoft.com/sharepoint/dsp" version="1.0" exclude-result-prefixes="xsl msxsl ddwrt" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:asp="http://schemas.microsoft.com/ASPNET/20" xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:SharePoint="Microsoft.SharePoint.WebControls" xmlns:ddwrt2="urn:frontpage:internal"> <xsl:output method="html" indent="no"/> <xsl:param name="URL"></xsl:param> <xsl:param name="SpeakerID"></xsl:param> <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"> <xsl:call-template name="Speakers"/> </xsl:template> <xsl:template name="Speakers"> <xsl:variable name="Rows" select="/dsQueryResponse/Speakers/Rows/Row[ @ID = $SpeakerID or $SpeakerID = '*' ]"/> <table class="spstc-speakers" border="0" cellpadding="2" cellspacing="0"> <tr> <td class="spstc-speakers-header"> <div class="title">Speakers</div> </td> </tr> <tr> <td> <span class="spstc-speaker-type-link"> <xsl:if test="$SpeakerID = '*'"> <xsl:attribute name="class">spstc-speaker-type-link spstc-selected</xsl:attribute> </xsl:if> <a href="{$URL}"> All (<xsl:value-of select="count(/dsQueryResponse/Speakers/Rows/Row)"/>) </a> </span> </td> </tr> <xsl:for-each select="$Rows"> <xsl:sort select="@Last_x0020_Name" order="ascending"/> <xsl:sort select="@First_x0020_Name" order="ascending"/> <xsl:call-template name="Speakers.rowview" /> </xsl:for-each> </table> </xsl:template> <xsl:template name="Speakers.rowview"> <xsl:variable name="NewSpeaker" select="ddwrt:NameChanged(string(@Title), 0)"/> <xsl:if test="string-length($NewSpeaker) > 0"> <tr> <td class="spstc-speaker-title"> <xsl:value-of select="@Title"/> </td> </tr> </xsl:if> <tr> <td> <xsl:if test="string-length(@Job_x0020_Title) > 0"> <xsl:value-of select="@Job_x0020_Title"/> </xsl:if> <xsl:if test="string-length(@Job_x0020_Title) > 0 and string-length(@Company) > 0"> <span> at </span> </xsl:if> <xsl:if test="string-length(@Company) > 0"> <xsl:value-of select="@Company"/> </xsl:if> <xsl:if test="string-length(@Blog) > 0"> <span class="spstc-speakers-label"> <a href="{@Blog}"> Blog</a></span> </xsl:if> <xsl:if test="string-length(@Twitter) > 0"> <span class="spstc-speakers-label"> <a href="http://twitter.com/{@Twitter}"> <xsl:choose> <xsl:when test="starts-with(@Twitter, '@')"> <xsl:value-of select="concat(' ', @Twitter)"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="concat(' @', @Twitter)"/> </xsl:otherwise> </xsl:choose> </a> </span> </xsl:if> <xsl:if test="@MCM.value = 1"> <span class="spstc-speakers-label"> MCM</span> </xsl:if> <xsl:if test="@MVP.value = 1"> <span class="spstc-speakers-label"> MVP</span> </xsl:if> </td> </tr> <xsl:if test="string-length(@Bio) > 0"> <tr> <td> <span class="spstc-speaker-bio"><xsl:value-of select="@Bio" disable-output-escaping="yes"/></span> </td> </tr> </xsl:if> <tr> <td> <xsl:call-template name="Sessions"> <xsl:with-param name="ThisSpeakerID" select="concat(@ID, ';#', @Title)"/> </xsl:call-template> </td> </tr> </xsl:template> <xsl:template name="Sessions"> <xsl:param name="ThisSpeakerID"/> <xsl:variable name="Rows" select="/dsQueryResponse/Sessions/Rows/Row[ contains(@Speaker., $ThisSpeakerID) and @_ModerationStatus = 'Approved' ]"/> <xsl:if test="count($Rows) > 0"> <table class="spstc-speaker-sessions" border="0" cellpadding="2" cellspacing="0"> <xsl:for-each select="$Rows"> <xsl:call-template name="Sessions.rowview" /> </xsl:for-each> </table> </xsl:if> </xsl:template> <xsl:template name="Sessions.rowview"> <tr> <td> <a href="/SitePages/Sessions.aspx?SessionID={@ID}"><xsl:value-of select="@Title"/></a> </td> </tr> </xsl:template> </xsl:stylesheet>
There are a few special parameters which I’m using in the Speakers page’s XSL:
URL is a parameter which I find I almost always set up in my DVWPs. It comes from an IIS Server Variable called URL and simply contains the current page’s URL. By grabbing that and using it wherever I can, it ensures that the XSL will usually work if I move it to a different location. (I always try to limit the number of literal strings in my XSL where I can.)
The page also accepts a SpeakerID on the Query String. If a value is present, the page filters to only that one speaker. This is useful in clicking over to this page from the Sessions page.
The Sessions Page
The Sessions list is simpler, but the page is a little more complicated. On the Sessions page, we’re more likely to want to filter and sort. At the top of the page, we have a set of filters (one per session type), as well as some sorting choices. We didn’t try to provide every slice and dice opportunity we could think of; just the most common ones.
As with the Speakers page, I won’t go through the XSL line by line here, but here’s a brief overview of the templates:
- Sessions – This template gets all of the sessions from the Sessions list and adds the filters and sorting capabilities to the page.
- Sessions.body – We needed a separate body template this time because we wanted to do several types of sorting.
- Sessions.body.SpeakerSort – In fact, because speakers are Person or Group columns, we needed a special template just to sort them appropriately.
- SessionTypes – This template lists out the session types as links for the filtering capability.
- Sessions.rowview – Here we display the individual session details.
- DisplaySpeakers – Because we can have 1-n speakers, I needed to build this recursive template. If you’re interested in recursive templates like this, check out my SPXSLT Codeplex Project, where I offer up quite a few others.
- SortLink – We needed the sort links to work either ascending or descending and to change based on the last sort requested. This template handles all of that generically and works for any LinkText and ColumnName I pass into it.
<xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:d="http://schemas.microsoft.com/sharepoint/dsp" version="1.0" exclude-result-prefixes="xsl msxsl ddwrt" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:asp="http://schemas.microsoft.com/ASPNET/20" xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:SharePoint="Microsoft.SharePoint.WebControls" xmlns:ddwrt2="urn:frontpage:internal"> <xsl:output method="html" indent="no"/> <xsl:param name="URL"></xsl:param> <xsl:param name="SessionType"></xsl:param> <xsl:param name="SessionID"></xsl:param> <xsl:param name="SortColumn"></xsl:param> <xsl:param name="SortDir"></xsl:param> <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"> <xsl:call-template name="Sessions"/> </xsl:template> <xsl:template name="Sessions"> <xsl:variable name="Rows" select="/dsQueryResponse/Sessions/Rows/Row[@_ModerationStatus = 'Approved']"/> <table class="spstc-sessions" border="0" cellpadding="2" cellspacing="0"> <tr> <td class="spstc-sessions-header"> <div class="title">Sessions</div> </td> </tr> <tr> <td> <span class="spstc-session-type-link"> <xsl:if test="$SessionType = '*'"> <xsl:attribute name="class">spstc-session-type-link spstc-selected</xsl:attribute> </xsl:if> <a href="{$URL}"> All (<xsl:value-of select="count($Rows)"/>) </a> </span> <xsl:for-each select="$Rows[string-length(@Session_x0020_Type) >0]"> <xsl:sort select="@Session_x0020_Type" order="ascending"/> <xsl:call-template name="SessionTypes"> <xsl:with-param name="Rows" select="$Rows"/> </xsl:call-template> </xsl:for-each> </td> </tr> <tr> <td> Sort by: <xsl:call-template name="SortLink"> <xsl:with-param name="LinkText" select="'Session Name'"/> <xsl:with-param name="ColumnName" select="'Title'"/> </xsl:call-template> <xsl:call-template name="SortLink"> <xsl:with-param name="LinkText" select="'Lead Speaker'"/> <xsl:with-param name="ColumnName" select="'Speaker.'"/> </xsl:call-template> <xsl:call-template name="SortLink"> <xsl:with-param name="LinkText" select="'Session Level'"/> <xsl:with-param name="ColumnName" select="'Session_x0020_Level'"/> </xsl:call-template> <xsl:call-template name="SortLink"> <xsl:with-param name="LinkText" select="'Session Type'"/> <xsl:with-param name="ColumnName" select="'Session_x0020_Type'"/> </xsl:call-template> </td> </tr> <xsl:choose> <xsl:when test="$SortColumn = 'Speaker.'"> <xsl:call-template name="Sessions.body.SpeakerSort"> <xsl:with-param name="Rows" select="$Rows"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:call-template name="Sessions.body"> <xsl:with-param name="Rows" select="$Rows"/> </xsl:call-template> </xsl:otherwise> </xsl:choose> </table> </xsl:template> <xsl:template name="Sessions.body"> <xsl:param name="Rows"/> <xsl:for-each select="$Rows[ ($SessionType != '*' and @Session_x0020_Type = $SessionType) or ($SessionID != '*' and @ID = $SessionID) or ($SessionType = '*' and $SessionID = '*') ]"> <xsl:sort select="@*[name()=$SortColumn]" order="{$SortDir}" /> <xsl:call-template name="Sessions.rowview" /> </xsl:for-each> </xsl:template> <xsl:template name="Sessions.body.SpeakerSort"> <xsl:param name="Rows"/> <xsl:for-each select="$Rows[ ($SessionType != '*' and @Session_x0020_Type = $SessionType) or ($SessionID != '*' and @ID = $SessionID) or ($SessionType = '*' and $SessionID = '*') ]"> <xsl:sort select="/dsQueryResponse/Speakers/Rows/Row[@ID = substring-before(current()/@Speaker., ';#')]/@Last_x0020_Name" order="{$SortDir}" /> <xsl:call-template name="Sessions.rowview" /> </xsl:for-each> </xsl:template> <xsl:template name="SessionTypes"> <xsl:param name="Rows"/> <xsl:variable name="NewSessionType" select="ddwrt:NameChanged(string(@Session_x0020_Type), 0)"/> <xsl:if test="string-length($NewSessionType) > 0"> <span class="spstc-session-type-link"> <xsl:if test="$SessionType = @Session_x0020_Type"> <xsl:attribute name="class">spstc-session-type-link spstc-selected</xsl:attribute> </xsl:if> <a href="{$URL}?SessionType={@Session_x0020_Type}"> <xsl:value-of select="@Session_x0020_Type"/> (<xsl:value-of select="count($Rows[@Session_x0020_Type = current()/@Session_x0020_Type])"/>) </a> </span> </xsl:if> </xsl:template> <xsl:template name="Sessions.rowview"> <tr> <td class="spstc-session-title"> <xsl:value-of select="@Title"/> </td> </tr> <tr> <td> <span class="spstc-sessions-label">Speaker(s): </span> <xsl:call-template name="DisplaySpeakers"> <xsl:with-param name="Speakers" select="@Speaker."/> </xsl:call-template> </td> </tr> <tr> <td> <span class="spstc-sessions-label">Session Level: </span> <xsl:value-of select="@Session_x0020_Level"/> </td> </tr> <tr> <td> <span class="spstc-sessions-label">Session Type: </span> <xsl:value-of select="@Session_x0020_Type"/> </td> </tr> <tr> <td class="spstc-session-description"> <xsl:value-of select="@Description" disable-output-escaping="yes"/> </td> </tr> </xsl:template> <!-- 110;#Joel Ward;#109;#Dave Shimko --> <xsl:template name="DisplaySpeakers"> <xsl:param name="Speakers"/> <xsl:param name="MultiSelectDelimiter" select="';#'"/> <xsl:param name="MultiSelectSeparator"/> <xsl:variable name="ThisSpeakerID" select="substring-before($Speakers, $MultiSelectDelimiter)"/> <xsl:variable name="ThisSpeakerName"> <xsl:choose> <xsl:when test="string-length(substring-before(substring-after($Speakers, $MultiSelectDelimiter), $MultiSelectDelimiter)) > 0"> <xsl:value-of select="substring-before(substring-after($Speakers, $MultiSelectDelimiter), $MultiSelectDelimiter)"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="substring-after($Speakers, $MultiSelectDelimiter)"/> </xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:variable name="OtherSpeakers" select="substring-after($Speakers, concat($MultiSelectDelimiter, $ThisSpeakerName, $MultiSelectDelimiter))"/> <a href="/SitePages/Speakers.aspx?SpeakerID={$ThisSpeakerID}"> <xsl:value-of select="$ThisSpeakerName"/> </a> <xsl:if test="string-length($OtherSpeakers) > 0"> <xsl:value-of select="', '"/> <xsl:call-template name="DisplaySpeakers"> <xsl:with-param name="Speakers" select="$OtherSpeakers"/> </xsl:call-template> </xsl:if> </xsl:template> <xsl:template name="SortLink"> <xsl:param name="LinkText"/> <xsl:param name="ColumnName"/> <xsl:variable name="ASortIMG" select="'/SiteAssets/spstc/images/ascending.png'"/> <xsl:variable name="DSortIMG" select="'/SiteAssets/spstc/images/descending.png'"/> <xsl:variable name="AscDesc"> <xsl:choose> <xsl:when test="$ColumnName = $SortColumn and (string-length($SortDir) = 0 or $SortDir = 'ascending')">descending</xsl:when> <xsl:otherwise>ascending</xsl:otherwise> </xsl:choose> </xsl:variable> <span class="spstc-sort-column-link"> <a href="{$URL}?SessionType={$SessionType}&SessionID={$SessionID}&SortColumn={$ColumnName}&SortDir={$AscDesc}"> <xsl:if test="$SortColumn = $ColumnName"> <xsl:attribute name="class">spstc-sort-column-link spstc-selected</xsl:attribute> </xsl:if> <xsl:value-of select="$LinkText"/> <span class="spstc-sort-indicator"> <xsl:choose> <xsl:when test="$ColumnName = $SortColumn and ($SortDir = '' or $SortDir = 'ascending')"> <img alt="Ascending - Click to Reverse" border="0" src="{$ASortIMG}"/> </xsl:when> <xsl:when test="$ColumnName = $SortColumn and $SortDir = 'descending'"> <img alt="Descending - Click to Reverse" border="0" src="{$DSortIMG}"/> </xsl:when> <xsl:otherwise> </xsl:otherwise> </xsl:choose> </span> </a> </span> </xsl:template> </xsl:stylesheet>
As with the Speakers page, there are some special parameters in use here:
I’ve already explained URL, and the SessionID works basically the same way as the SpeakerID. If we pass a SessionID, then only that one session is shown.
SessionType is also used as a filtering mechanism. I like to use “*” for “All”, just like when you use an asterisk for wildcard searches.
The SortColumn and SortDir Query String parameters drive the sorting capability. I mimic what SharePoint does in List View Web Parts here when you sort using the column headers.
The CSS
There isn’t a lot of unique CSS for all of this, but I did write some. It’s probably not as lean as some would do, but it works. If you only use the Common Dialogs, the custom CSS is messy, embedded in the page, and can’t be used elsewhere. I usually move it into a separate CSS file. You can see where I’ve applied the classes above. You’ve probably noticed that I’m still sort of fond of table-based layouts. I get the whole “tables are evil” thing, but in cases like this, I find that tables work great.
.spstc-sessions, .spstc-speakers { margin-left:100px; font-size:12px; color:#4f4e4e; } .spstc-sessions td, .spstc-speakers td { padding-bottom:5px; padding-left:10px; padding-top:5px; } .spstc-session-type-link, .spstc-speaker-type-link { padding-right:5px; margin-right:5px; border-right:2px #4f4e4e solid; } span.spstc-sort-column-link { padding-left:5px; padding-right:5px; border-right:2px #4f4e4e solid; vertical-align:middle; } .spstc-sort-indicator { padding-left:2px; } .spstc-session-title, .spstc-speaker-title { line-height: 150%; border-bottom:5px #ffffff solid; border-top:5px #ffffff solid; background-color:#4f4e4e; font-size:12px; font-weight:bold; color:#ffffff; } .spstc-sessions-label, .spstc-speakers-label { font-weight:bold; } .spstc-session-description { padding-bottom:20px; } .spstc-speaker-bio { padding-bottom:5px; } .spstc-selected { font-weight:bold; }
Conclusion
Well, there you have it. Now you have a lot of the main ingredients to setup a SharePoint-driven conference site. Well, at least the Speakers and Sessions bits. You still have to carry all of those boxes and get all the speakers to show up with something to say. This is the easy part!
Remember, we’re going to do a live session on August 16 at 2pm EDT [register] which will go into more detail on all of this. If you’ve read this far, you probably have a few questions. Be sure to bring ’em along and join us!
Thanks for the information Marc. Think you could roll all this into a downloadable solution on codeplex?
Kanwal:
What’s wrong with good old copy and paste? In any case, as with most of my posts, this should be considered a working example. It’s not likely to directly solve anyone else’s business problem, but there are bits and pieces of it which may help solve things.
M.
Really nice job with that. The initial version was a bit…shaky, and I definitely noticed a huge improvement in ui and look and feel last week!
Hey Marc, I only just realized you had a hand in the spstcdc site. If I build the SP lists to support an interactive SP Maturity Model, will you help me with some middle tier magic there as well? (Guess I should attend the webinar, huh?)
Sadie:
Yes, attend the webinar! But I’d be happy to help, of course.
M.