For the last year and half I have increasingly turned to one module as my goto solution when building out new capabilities.  For me the Reports Module is proving itself to be the Swiss Army knife of the DotNetNuke module world.  With some of the features coming up in future releases of the Reports module this will become even more evident to anyone who takes the time to learn how to use the module.

So why do I feel so strongly about the reports module? It is mainly because of the architecture that Andrew Nurse put in place to allow you to create custom data sources and custom visualizers.  This architecture makes it easy to get data from almost anywhere and then to have complete control over how it is displayed. 

For most people the built in DotNetNuke Data Source, and XSLT Visualizer can handle most of your needs.  You can query any data in DotNetNuke and then use XSLT to display that data.  If you have not used XSLT before, you are missing out on a very powerful tool.  Whether it is displaying a blogroll, the MarketPlace Monitor or even building an entire eCommerce site, XSLT gives you a lot of capability to take XML data and transform it into as simple or complex a page as you can imagine.

Right now there are three core modules with extensive XSLT capability built in: Forms and Lists, XML, and Reports.  Forms and Lists is a great module, but it forces you to store data in the tables it defines.  XML also has great XSLT functionality, but it only handles XML that is stored in a file or that is retrieved from a URL.  If I want to report on Data, no matter where it is stored, my only option is the Reports module.  The Reports module includes a custom data source that allows you to read UDT data, and it wouldn’t be hard to write a custom data source to read an XML file.

Even though I love the Reports module, the Forms and Lists, and the XML modules both have some features that are not currently supported by the XSLT engine in the Reports module.  They all have their place in your DotNetNuke arsenal.

So if your need is just to display some data on the page, and you want something more than a simple table, then the Reports module is the tool for you.  In the rest of this post I’ll walk through a recent requirement I had for our company intranet.

One common requirement in many intranets is that it should be easy for employees to find out basic contact and organizational information on other employees.  DotNetNuke includes much of this information in the default user profile.  For my purposes I wanted to display the following pieces of employee information:

  • Name
  • Photo
  • Job Title
  • Department
  • Manager
  • Phone Numbers (Work, Cell, Fax)
  • IM
  • Email
  • Location (Region, Country and Time Zone)

With my data elements identified, I can use the built in User Accounts screen to access the Manage Profile Properties page.  From here I can add any profile elements I want to capture.  The default set of profile elements cover most of my requirements, and it is easy enough to add any fields which are missing. Now employees can edit their own profiles and we’ll use the reports module to display those profiles. 

ManageProfile

To configure the reports module, you will need to be logged in with a Host account.  The reason for this requirement is that the SQL query we are building could potentially access any data in the database.  In a multi-portal environment you wouldn’t want one admin access data for another portal to which they didn’t have access.  Since hosts have unlimited access to all data in the system, they are needed for defining the data source. Once the data source is defined then any user with edit permissions will be able to configure the visualizer.

Go to the module settings page and navigate down to the Data Source Settings section.  For our example, I can use the DotNetNuke Data Source which uses the default dataprovider defined in the web.config.  Then I enter my query.  Finally, I will use the Show Xml Source link to display the resulting XML from my data source query.  I will save this xml to a separate file so that I can test my XSLT during development.

ReportDataSource

Notice in my query that I just select all of the data from a custom view.  You will not have this view on your DotNetNuke installation.  One limitation I found very early with the Reports module is that it stores all of its configuration data in the ModuleSettings and TabModuleSettings tables.  This limits the amount of text I can use for the query.  I know from experience that querying profile data can get very complex and will easily exceed the size limitation of the settings tables.  As a result I almost always create a custom view or stored procedure for my query.  Because each piece of profile data is stored in a separate row of the UserProfile table, we have to do a join between the User table and UserProfile table for each property we want to include.  As you can see from the query below this quickly becomes quite long, although most of the logic is just repeated over and over.

SELECT     
    u.UserID, 
    u.FirstName, 
    u.LastName, 
    u.Username, 
    u.Email, 
    u.DisplayName, 
    up1.PropertyValue AS Region, 
    up2.PropertyValue AS Country, 
    up3.PropertyValue AS Telephone, 
    up4.PropertyValue AS Cellphone, 
    up5.PropertyValue AS IM, 
    up6.PropertyValue AS JobTitle, 
    up7.PropertyValue AS Manager, 
    up8.PropertyValue AS Department, 
    up9.PropertyValue AS Fax
FROM
    dbo.Users AS u INNER JOIN
    dbo.UserRoles AS ur ON u.UserID = ur.UserID INNER JOIN
    dbo.Roles AS r ON ur.RoleID = r.RoleID AND 
    r.RoleName = N'Employee' LEFT OUTER JOIN
    (SELECT     
        up.UserID, 
        up.PropertyValue
    FROM
        dbo.UserProfile AS up INNER JOIN
        dbo.ProfilePropertyDefinition AS ppd ON 
        up.PropertyDefinitionID = ppd.PropertyDefinitionID AND 
        ppd.PropertyName = 'Region' AND 
        ppd.PortalID = 0) AS up1 ON u.UserID = up1.UserID LEFT OUTER JOIN
    (SELECT     
        up.UserID, 
        up.PropertyValue
    FROM
        dbo.UserProfile AS up INNER JOIN
        dbo.ProfilePropertyDefinition AS ppd ON 
        up.PropertyDefinitionID = ppd.PropertyDefinitionID AND 
        ppd.PropertyName = 'Country' AND 
        ppd.PortalID = 0) AS up2 ON u.UserID = up2.UserID LEFT OUTER JOIN
    (SELECT     
        up.UserID, 
        up.PropertyValue
    FROM
        dbo.UserProfile AS up INNER JOIN
        dbo.ProfilePropertyDefinition AS ppd ON 
        up.PropertyDefinitionID = ppd.PropertyDefinitionID AND 
        ppd.PropertyName = 'Telephone' AND 
        ppd.PortalID = 0) AS up3 ON u.UserID = up3.UserID LEFT OUTER JOIN
    (SELECT
        up.UserID, 
        up.PropertyValue
    FROM
        dbo.UserProfile AS up INNER JOIN
        dbo.ProfilePropertyDefinition AS ppd ON 
        up.PropertyDefinitionID = ppd.PropertyDefinitionID AND 
        ppd.PropertyName = 'Cell' AND 
        ppd.PortalID = 0) AS up4 ON u.UserID = up4.UserID LEFT OUTER JOIN
    (SELECT
        up.UserID, 
        up.PropertyValue
    FROM
        dbo.UserProfile AS up INNER JOIN
        dbo.ProfilePropertyDefinition AS ppd ON 
        up.PropertyDefinitionID = ppd.PropertyDefinitionID AND 
        ppd.PropertyName = 'IM' AND 
        ppd.PortalID = 0) AS up5 ON u.UserID = up5.UserID LEFT OUTER JOIN
    (SELECT
        up.UserID, 
        up.PropertyValue
    FROM
        dbo.UserProfile AS up INNER JOIN
        dbo.ProfilePropertyDefinition AS ppd ON 
        up.PropertyDefinitionID = ppd.PropertyDefinitionID AND 
        ppd.PropertyName = 'Job_Title' AND 
        ppd.PortalID = 0) AS up6 ON u.UserID = up6.UserID LEFT OUTER JOIN
    (SELECT
        up.UserID, 
        up.PropertyValue
    FROM
        dbo.UserProfile AS up INNER JOIN
        dbo.ProfilePropertyDefinition AS ppd ON 
        up.PropertyDefinitionID = ppd.PropertyDefinitionID AND 
        ppd.PropertyName = 'Manager' AND 
        ppd.PortalID = 0) AS up7 ON u.UserID = up7.UserID LEFT OUTER JOIN
    (SELECT
        up.UserID, 
        up.PropertyValue
    FROM
        dbo.UserProfile AS up INNER JOIN
        dbo.ProfilePropertyDefinition AS ppd ON 
        up.PropertyDefinitionID = ppd.PropertyDefinitionID AND 
        ppd.PropertyName = 'Department' AND 
        ppd.PortalID = 0) AS up8 ON u.UserID = up8.UserID LEFT OUTER JOIN
    (SELECT
        up.UserID, 
        up.PropertyValue
    FROM
        dbo.UserProfile AS up INNER JOIN
        dbo.ProfilePropertyDefinition AS ppd ON 
        up.PropertyDefinitionID = ppd.PropertyDefinitionID AND 
        ppd.PropertyName = 'Fax' AND 
        ppd.PortalID = 0) AS up9 ON u.UserID = up9.UserID

Now that we have our data in an xml format (you did remember to use the Show XML Source and save the output to a file, didn’t you) we can turn our attention to defining our XSLT.  Because our final output is html, I like to mockup my output in a web editor.  This makes the XSLT much easier, as editing HTML when you have a bunch of XSLT logic interspersed can get a little tricky depending on the complexity.  Using Expression web I am able to quickly mockup a simple display using the elements previously described.

ReportHtmlTemplate

As you can see in the HTML and CSS below, it is not too complicated (and it doesn’t use any tables).

<div class="employee">
    <img alt="No Photo Present" height="100" src="No.Photo.jpg" width="75" />
    <div class="organization">
        <h2>Joe Brinkman</h2>
        <p class="title">Technical Fellow, Co-Founder</p>
        <p><span>Department:</span>Engineering </p>
        <p><span>Reports to:</span> Shaun Walker</p>
    </div>
    <div class="contact">
        <h3>Contact Information</h3>
        <p><span>Work Phone:</span> 555-1212</p>
        <p><span>Mobile Phone:</span> 555-2121</p>
        <p><span>IM:</span> <a href="mailto:joe.brinkman@tag-software.net">joe.brinkman@tag-software.net</a></p>
        <p><span>Location:</span>&nbsp; OH, United States</p>
    </div>
</div>
.employee 
{
    padding: 10px;
    border: solid 1px silver;
    margin: 10px;
}
.employee .organization {
    float: left;
    width: 275px;
}
.employee .contact {
    float: left;
    width: 350px;
}
.employee img {
    float: left;
    margin-right: 10px;
}
.employee h2 {
    font-size: 1.2em;
    margin: 5px 0px 5px 0px;
    color: #800000;
}
.employee h3 {
    font-size: 1.1em;
    color: #333;
    margin: 5px 0px 5px 0px;
}
.employee p {
    margin: 0px;
}
.employee p.title {
    font-style: italic;
    margin: -5px 0px 10px 0px;
}
.employee span {
    display: inline-block;
    width: 120px;
    text-align: right;
    padding-right: 5px;
    font-weight: bold;
    color: #808080
}

Now we are ready to create our XSLT.  We know what our HTML should look like and we’ll be able to do a quick test as we go to ensure that everything is working correctly.  Visual Studio 2008 includes pretty decent XSLT support, but you can use whatever editor with which you are comfortable.  I used to use XMLSpy from Altova, but have found the XSLT support in VS 2008 meets most of my needs.

Using my HTML as a template, I am able to quickly put together the XSLT below.  Because my final output is going to be HTML, I indicate that in the XSL:Output tag.  Then I do a quick match on all elements in “//DocumentElement” which is my root document element.  I use this template because I have some html which should only be included on the page one time (the root element by definition only appears once).  From there I apply another template for all of the QueryResults nodes in the XML document.  You will have one node for each record returned by your query.  This second template is where I insert my HTML snippet.

In the QueryResults template it is a simple matter to replace all of the static data in my sample HTML with an <xsl:value-of select="ElementName" /> tag.  You can see my first cut at the XSLT below.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" >
    
    <xsl:output method="html" indent="yes"/>

    <xsl:variable name="baseDir">http://extranet.dotnetnuke.com/portals/0/Employee</xsl:variable>
    
    <xsl:template match="//DocumentElement">
        <link href="{$baseDir}/employee.css" rel="stylesheet" type="text/css" />
        
        <xsl:apply-templates select="QueryResults" />

    </xsl:template>
    
    <xsl:template match="QueryResults">
        <div class="employee">
            <img alt="No Photo Present" height="100" src="{$baseDir}/No.Photo.jpg" width="75" />
            <div class="organization">
                <h2>
                    <xsl:value-of select="DisplayName" />
                </h2>
                <p class="title">
                    <xsl:value-of select="JobTitle" />
                </p>
                <p>
                    <span>Department:</span>
                    <xsl:value-of select="Department" />
                </p>
                <p>
                    <span>Reports to: </span>
                    <xsl:value-of select="Manager" />
                </p>
            </div>
            <div class="contact">
                <h3>Contact Information</h3>
                <p>
                    <span>Work Phone:</span>
                    <xsl:value-of select="Telephone" />
                </p>
                <p>
                    <span>Mobile Phone:</span>
                    <xsl:value-of select="Cellphone" />
                </p>
                <p>
                    <span>MSN IM:</span>
                    <xsl:value-of select="IM" />
                </p>
                <p>
                    <span>Location:</span><xsl:value-of select="Region" />, <xsl:value-of select="Country" />
                </p>
            </div>
            <p style="clear:both;"> </p>
        </div>
    </xsl:template>
</xsl:stylesheet>

If you understand the HTML from my original template then the XSLT should be pretty straight forward.  Now that I have the XSLT complete I can use Visual Studio to show me the sample rendering.  Select the Show XSLT Output from the XML menu.

ReportsXSLTTest

Visual Studio will ask you for an XML file to use – you should select the XML file with the data we created earlier from our data source.  Visual Studio will now render the HTML output.  If everything goes correctly you are done.  If the display is not exactly what you want then you can tweak the XSLT until everything is perfect.  To help with this, Visual Studio allows you to debug the XSLT.  This means you can set break-points and examine different values as you step through the XSLT.  While that is probably not needed in this case, when you get into more complex pages, you will certainly appreciate this feature.

In this example I have left out some of the data elements and I haven’t included any complex logic.  If this is your first time with XSLT then I definitely recommend that you start with something easy like the above example.  With a little experience you will find that XSLT is a pretty full-featured language.  There are some idiosyncrasies to get used to, but once you get into it, you will be amazed at the power and flexibility it provides. 

The next version of the Reports module includes support for XsltExtension objects.  This basically allows you to call custom .Net code from inside your XSLT.  This means you could apply custom business logic in your XSLT that would be impossible to do in XSLT or SQL alone.  You could do something like custom filtering of the displayed data based on the current user’s permissions.  You could call a webservice in a custom extension and then merge the data with the rest of the data being displayed.  You could do browser sniffing to determine how best to format the HTML output. With XsltExtensions you really remove any limitation imposed by the original DataSource or the XSLT language.

Now that you have the final XSLT, you can configure the XSL Transformation Visualizer.  In this case I have uploaded my Employee.xsl file and left the rest of the settings with the default values.  Depending on your data, you might need to do HTML Encoding or Decoding.  Once everything is working correctly, you should definitely enable caching as well.

ReportVisualizer

If you have been following along you should now end up with a display something like the one below.  You can substitute your own “nophoto” picture and set the baseDir in the XSLT, but everything else should work without any difficulty.

ReportFinal

At this point I hope I have shown you some of the power of the Report module when combined with the XSLT Visualizer.  I will leave it as an excercise to the reader to figure out how to handle sorting and paging and how to determine the photo to display.  There are several solutions you can choose depending on your skill and creativity.  For more help on XSLT and XPATH I highly recommend www.w3schools.com.  It has just enough information to give you a strong foundation without overwhelming you.  I would definitely be interested to see how you are using the Report module or other XSLT based solutions in your own websites.

UPDATE:

After reading Greg Lahens comment I went and did some research to improve my SQL.  I found a great article on Cross tabs and Pivots on SQLServerCentral.com (may require registration). Below is a Query which is much more optimized over the previous version.  While there are still a few minor tweaks that could be done, those tweaks would not significantly improve performance and would require the use of SQL 2005+, so I decided to stick with a simple cross-tab query.  This query dropped the number of reads to one fifth the amount over the previous query (for a small dataset) and as Greg points out, this version is much more readable and maintainable.

SELECT
    u.UserID, 
    u.FirstName, 
    u.LastName, 
    u.Username, 
    u.Email, 
    u.DisplayName, 
    upd.Region, 
    upd.Country, 
    upd.Telephone, 
    upd.Cellphone, 
    upd.IM, 
    upd.JobTitle, 
    upd.Manager, 
    upd.Department, 
    upd.Fax
FROM 
    dbo.Users AS u INNER JOIN
    dbo.UserRoles AS ur ON u.UserID = ur.UserID INNER JOIN
    dbo.Roles AS r ON ur.RoleID = r.RoleID AND r.RoleName = N'Employee' LEFT OUTER JOIN
(SELECT     
    up.UserID, 
    MAX(CASE WHEN ppd.PropertyName = 'Region' THEN up.PropertyValue ELSE '' END) AS Region,
    MAX(CASE WHEN ppd.PropertyName = 'Country' THEN up.PropertyValue ELSE '' END) AS Country,
    MAX(CASE WHEN ppd.PropertyName = 'Telephone' THEN up.PropertyValue ELSE '' END) AS Telephone,
    MAX(CASE WHEN ppd.PropertyName = 'Cell' THEN up.PropertyValue ELSE '' END) AS Cellphone,
    MAX(CASE WHEN ppd.PropertyName = 'IM' THEN up.PropertyValue ELSE '' END) AS IM,
    MAX(CASE WHEN ppd.PropertyName = 'Job_Title' THEN up.PropertyValue ELSE '' END) AS JobTitle,
    MAX(CASE WHEN ppd.PropertyName = 'Manager' THEN up.PropertyValue ELSE '' END) AS Manager,
    MAX(CASE WHEN ppd.PropertyName = 'Department' THEN up.PropertyValue ELSE '' END) AS Department,
    MAX(CASE WHEN ppd.PropertyName = 'Fax' THEN up.PropertyValue ELSE '' END) AS Fax
FROM          
    dbo.UserProfile AS up INNER JOIN
    dbo.ProfilePropertyDefinition AS ppd ON up.PropertyDefinitionID = ppd.PropertyDefinitionID and ppd.PortalID = 0
Group By up.UserID) as upd on u.UserID = upd.UserID