Download сomplete source code for the article - 3.8 KB
Live demo for the tutorial
Introduction
DotNetNuke 5.2+ goes with powerful Telerik control library. As far as Telerik Controls is a typical ASP.NET library one can think that he need a Visual Studio to create modules using amazing Telerik controls and features. Here I’ll show you how to add Telerik Controls to you DotNetNuke site pages without need to develop and deploy any modules. All tools you need are free and open source. You need a DotNetNuke community Edition 5.2 or higher and XsltDb 01.01.24 or higher.
A very nice task for this Telerik-DotNetNuke tutorial is to create a page rating/comments system. For the rating I take two Telerik controls: RadRating and RadChart and implement a rating system similar to the codeproject article rating. Rating subsystem must support the following features:
- Display current page rating,
- Display votes histogram: how votes are distributed by value,
- Let visitor to vote,
- Control visitor votes using cookie, if visitor already voted system deletes previous vote. This allows visitor to correct/change their votes but prevent votes duplication.
For the comments I take RadEditor for the comment text and RadComboBox for the Name / E-Mail / Site input boxes. Why Combo? RadTextBox doesn’t support rounded corners. But It is possible to setup a combo to enter text without drop down feature. I believe Telerik will support rounded corners for RadTextBox and RadEditor in future releases. Comments subsystem must support the following features:
- List of comments in chronological order,
- Comment submission form,
- Commenter's avatar,
- Comment deletion for site admin,
- New comments E-Mail notification.
One thing is left to define. What is the object of rating/commenting? Actually, we don’t need to know what this object is we just need it’s ID. In the web we can reliably use URL as content ID. Real URL may contain additional parameters that don’t identify content so we must filter them out when building ID of content to rate/comment.
You can view it live before reading
Content Identification
There are 2 methods of getting current URL
- After URL rewrite: mdo:aspnet('Request.Url.PathAndQuery')
- Before URL Rewrite: mdo:aspnet('Request.RawUrl')
You must decide what method is suitable for you. I use here original client URL. So PageID is built as follows:
<xsl:variable name="page-id" select="mdo:aspnet('Request.RawUrl')" />
To deliver reliable portal identification we use relative URLs and XsltDb’s feature of data isolation. So the database table for the identified content will be the following:
create table {databaseOwner}MDO_PageRating
(
PageID int identity primary key,
PortalID int,
PageGuid nvarchar(256)
)
To simplify content identification we use the following combined stored procedure that checks if the page already identified and if not – just insert a new record and returns it’s ID
create procedure {databaseOwner}MDO_PageRating_EnsurePage
@PortalID int,
@PageGuid nvarchar(max),
@PageID int out
as
begin
select @PageID = PageID from MDO_PageRating
where PageGuid = @PageGuid and PortalID = @PortalID
if @PageID is null begin
insert {databaseOwner}MDO_PageRating(PortalID, PageGuid)
values(@PortalID, @PageGuid);
set @PageID = SCOPE_IDENTITY();
end;
end;
)
Now we are ready to begin with rating.
Rating Subsystem
Database objects
First we must create a table that stores each vote
create table {databaseOwner}MDO_PageRating_Rate
(
RateID int identity primary key,
PageID int constraint MDO_PageOfRating foreign key references MDO_PageRating(PageID)
on delete cascade on update cascade,
Value float,
Cookie uniqueidentifier
)
And we need 2 stored procedures: to register a new vote and to retrieve information about votes. New vote registration:
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_rate_page]
@PortalID int,
@PageGuid nvarchar(max),
@Value float,
@Cookie nvarchar(max)
as
begin
declare @PageID int;
exec {databaseOwner}MDO_PageRating_EnsurePage @PortalID, @PageGuid, @PageID out;
delete MDO_PageRating_Rate
where PageID = @PageID
and Cookie = @Cookie;
insert {databaseOwner}MDO_PageRating_Rate(PageID, Value, Cookie)
values(@PageID, @Value, @Cookie);
end;
Query for votes and rating:
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_page_rating]
@PortalID int,
@PageGuid nvarchar(max)
as
begin
select pr.* from {databaseOwner}MDO_PageRating_Rate pr
join {databaseOwner}MDO_PageRating p on p.PageID = pr.PageID
where p.PageGuid = @PageGuid
and p.PortalID = @PortalID;
select
SUM(StatCount) TotalVotes,
MAX(StatCount) Height,
SUM(StatValue * StatCount) / SUM(StatCount) Average
from (
select pr.Value StatValue, COUNT(*) StatCount
from {databaseOwner}MDO_PageRating_Rate pr
join {databaseOwner}MDO_PageRating p on p.PageID = pr.PageID
where p.PageGuid = @PageGuid
and p.PortalID = @PortalID
group by pr.Value) t;
end;
This stored procedure returns 2 result sets: list of votes and rating summary for the histogram. In rating summary we have total count of votes, maximum histogram bar height and current rating as average of votes.
Display current rating
To display current content rating we create 2 objects RadRating – it displays average rating of the page and RadChart – it displays votes distribution. So we make 3 steps
1. Read current page rating info from database
<xsl:variable name="rating" select="mdo:xml('page-rating', 'rating, total', $page-id)" />
2. Create a RadRating control to display current rating
<telerik:RadRating ID="PageRating" runat="server" Value="{mdo:coalesce($rating//Average, 0)}" ReadOnly="True" Skin="Office2007" />
3. Create RadChart control to display votes distribution
<telerik:RadChart runat="server" ID="RadChart2" ChartTitle-Visible="false" Width="100" Height="50" Skin="Office2007" >
<Series>
<telerik:ChartSeries Type="Bar">
<Items>
<xsl:for-each select="mdo:sequence(1,5,1)">
<telerik:ChartSeriesItem XValue="{current()}"
YValue="{count($rating//rating[Value=current()])+0.3}"
Label-TextBlock-Text="{count($rating//rating[Value=current()])}"/>
</xsl:for-each>
</Items>
</telerik:ChartSeries>
</Series>
<PlotArea Appearance-Dimensions-Margins="0,0,0,0">
<XAxis AutoScale="False" MinValue="0.5" MaxValue="5.5" Step="1" />
<YAxis AutoScale="False" MinValue="0"
MaxValue="{mdo:coalesce($rating//Height,0) + 1}"
Visible="False" />
</PlotArea>
<Legend Visible="false"/>
</telerik:RadChart>
Some fragments of listing above must be clarified.
- <xsl:for-each select="mdo:sequence(1,5,1)"> executes a loop from 1 to 5. XSL does not support loop variables so XsltDb provides a mdo:sequence function that returns XML-nodes that are actually the list of values for the loop variable. The current value can be captured using standard xsl function
current()
- YValue="{count($rating//rating[Value=current()])+0.3}" allows us to create a 0.3-height bar if there were no such votes.
- MaxValue="{mdo:coalesce($rating//Height,0) + 1}" assign a vertical scale to keep space for numbers above bars.
Rating input control
To let visitor vote we create another RadRating control as follows:
<telerik:RadRating ID="PageRating2" runat="server" Value="{mdo:coalesce($rating//Average, 0)}" OnClientRated="onClientRated" Skin="Office2007"
>
<Items>
<telerik:RadRatingItem Value="1" ToolTip="Poor" />
<telerik:RadRatingItem Value="2" ToolTip="Fair" />
<telerik:RadRatingItem Value="3" ToolTip="Good" />
<telerik:RadRatingItem Value="4" ToolTip="Excellent" />
<telerik:RadRatingItem Value="5" ToolTip="Awesome" />
</Items>
</telerik:RadRating>
For the better understanding stars meaning I assign a meaningful label for each star in RadRating Control. Initially it shows current rating of the page (Value="{mdo:coalesce($rating//Average, 0)}") and is subscribed to a rating event (OnClientRated="onClientRated"). Javascript function onClientRated is very simple
function onClientRated(sender, args)
{
{{mdo:submit('@rate', '@sender.get_value()')}}
}
It just get a value of rating provided by a visitor and submits it to server. Note that @-sign in @rate means that XsltDb keeps value of @rate only one request (command-like behavior) @-sign in submitting value means that this is javascript code and must be evaluated just before the submission.
After the rating is submitted to the server we must save it to the database. The code is simple:
<xsl:if test="mdo:param('@rate')">
<xsl:if test="not(mdo:cookie('rater'))">
<xsl:if test="mdo:set-cookie('rater', mdo:newid(), '2020-01-01')" />
</xsl:if>
<xsl:if test="mdo:xml('rate-page', '$script', $page-id, mdo:param('@rate'), mdo:cookie('rater'))" />
{{mdo:redirect(mdo:aspnet('Request.RawUrl'))}}
</xsl:if>
As you see only 3 steps are need to send ratind value to the database.
First, check if the visitor is identified. If not – create a cookie. I use mdo:newid() as a cookie value. mdo:newid() generates a guid so visitor is uniquely identified.
Second, send new rating value to the database. DB will check whether the visitor is new and insert a new rating or the visitor has already rated this content and DB replace previous vote.
Third, we redirect to the current page. This is not necessary but I suggest making this redirection because:
- You end a page lifecycle and begin new one, so your standard ASP.NET controls and Telerik controls go to their initial state. This let us avoid writing cleanup code that resets controls values to their initial state without breaking ASP.NET page lifecycle.
- When visitor pushes F5 it won’t see ugly message box whether he wants to resend data to server. And it also prevents double submission of data to server.
Rating subsystem is almost done, switching to comments subsystem
Comments Subsystem
Database objects
As for the rating here we need a table to store comments and 3 stored procedures to retrieve comment list, to save new comments to the database and to delete a comment by admin.
create table {databaseOwner}MDO_PageRating_Comment
(
CommentID int identity primary key,
ParentID int,
PageID int constraint MDO_PageOfComment foreign key references MDO_PageRating(PageID)
on delete cascade on update cascade,
Comment nvarchar(max),
EMail nvarchar(128),
URL nvarchar(128),
Name nvarchar(128),
DTCreated datetime default getdate()
)
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_comment_page]
@PortalID int,
@PageGuid nvarchar(max),
@ParentID int,
@Comment nvarchar(max),
@Name nvarchar(max),
@EMail nvarchar(max),
@URL nvarchar(max)
as
begin
declare @PageID int;
exec {databaseOwner}MDO_PageRating_EnsurePage @PortalID, @PageGuid, @PageID out;
if @ParentID < 1 set @ParentID = null;
insert {databaseOwner}MDO_PageRating_Comment(ParentID,PageID,Comment,EMail,URL,Name)
values(@ParentID,@PageID,@Comment,@EMail,@URL,@Name);
end;
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_page_comments]
@PortalID int,
@PageGuid nvarchar(max)
as
begin
select c.* from {databaseOwner}MDO_PageRating_Comment c
join {databaseOwner}MDO_PageRating p on p.PageID = c.PageID
where p.PageGuid = @PageGuid
and p.PortalID = @PortalID;
end;
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_delete_page_comment]
@PortalID int,
@CommentID int
as
begin
delete {databaseOwner}MDO_PageRating_Comment
where CommentID = @CommentID
and PageID in (select PageID from {databaseOwner} MDO_PageRating where PortalID = @PortalID);
end;
Comment input form
I love rounded corners, so I want to use Telerik controls to create rounded input boxes. The problem is that RadTextBox does not provide an option to round corners. There for I switch to use RadComboBox. The following code creates an input with rounded corners based on RadComboBox:
<telerik:RadComboBox runat="server" ShowToggleImage="False" ShowDropDownOnTextboxClick="False" AllowCustomText="True"/>
This approach is for the commenter’s Name, E-Mail and Web Site fields. I also want my comments be easy-to-read so I use RadEditor for the comment itself. I let only basic formatting for the commenter so I must enumerate tools that are allowed in comment window:
<telerik:RadEditor runat="server" ID="Message" EditModes="Design" Width="491px" Height="200px">
<Tools>
<telerik:EditorToolGroup >
<telerik:EditorTool Name="Bold" />
<telerik:EditorTool Name="Italic" />
<telerik:EditorTool Name="Underline" />
</telerik:EditorToolGroup>
<telerik:EditorToolGroup >
<telerik:EditorTool Name="InsertUnorderedList" />
<telerik:EditorTool Name="InsertOrderedList" />
</telerik:EditorToolGroup>
<telerik:EditorToolGroup >
<telerik:EditorTool Name="JustifyLeft" />
<telerik:EditorTool Name="JustifyCenter" />
<telerik:EditorTool Name="JustifyRight" />
<telerik:EditorTool Name="JustifyFull" />
</telerik:EditorToolGroup>
<telerik:EditorToolGroup >
<telerik:EditorTool Name="Cut" />
<telerik:EditorTool Name="Copy" />
<telerik:EditorTool Name="Paste" />
</telerik:EditorToolGroup>
</Tools>
</telerik:RadEditor>
Under the RadEditor I put a button that submits comment form
<input type="button" onclick="{mdo:jsubmit('@comment', 'yes')}" value="Save Comment" />
I want my "Send Comment" button to be skinned with Office2007 skin like others controls. For that purpose I use RadFormDecorator that allows me to apply a skin to a particular section of a page.
<telerik:RadFormDecorator runat="server" DecorationZoneID="rating-and-comments" Skin="Office2007"/>
<div id="rating-and-comments">
</div>
Server side comments processing
After comment is submitted we must put it into the database and notify site/page owner that the content was commented. He code is following:
<xsl:if test="mdo:param('@comment')">
<xsl:if test="mdo:xml('comment-page', '$script', $page-id, -1,
mdo:url-decode(mdo:request(mdo:client-name('Message'))),
mdo:request(mdo:client-name('Name')),
mdo:request(mdo:client-name('EMail')),
mdo:request(mdo:client-name('Url'))
)" />
<xsl:variable name="mail">
<p>
New comment added to the site http://xsltdb.com.
<a href="http://xsltdb.com{mdo:aspnet('Request.RawUrl')}">Click here to read comments.</a>
</p>
</xsl:variable>
<xsl:if test="mdo:mail('admin@xsltdb.com', 'admin@xsltdb.com', 'New comments added', mdo:text($mail))" />
{{mdo:redirect(mdo:aspnet('Request.RawUrl'))}}
</xsl:if>
Here we make 3 steps
1. mdo:xml('comment-page', '$script'…) saves new comment to the database. Here I want to clarify how values of ASP.NET controls can be retrieved. As usual, ASP.NET control has a hidden input associated that stores the value. You can get the name of the control using mdo:client-name() function. This function gets a local ID of ASP.NET control and returns global client name of the control’s hidden input. After that you can get a value using mdo:request() function. RadEditor stores an Url-encoded text in this input so we have to decode it before sending to the database using mdo:url-decode() function.
2. After comment is saved to the database we create and send a mail message to the site admin using
mdo:mail() function. This function sends a HTML message that we build as XSL variable and convert to string using mdo:text() function.
3. After processing is finished we restart ASP.NET page lifecycle using mdo:redirect() exactly as
we did it for the rating.
Displaying comments
To display a comments list for a page we first read comment list from database
<xsl:variable name="comments" select="mdo:xml('page-comments', 'comment', $page-id)" />
The following code displays comments list
<xsl:for-each select="$comments//comment">
<div style="width:100px;float:left;">
<img src="http://www.gravatar.com/avatar/{mdo:md5(EMail)}" />
<div style="color:grey">
<a rel="external nofollow" style="font-size:smaller" href="{URL}">{{Name}}</a>,
{{mdo:fmt-date(DTCreated, 'd')}}
</div>
</div>
<div style="float:left;background-color:white;width:375px;padding:10px;border:1px solid silver;">
{h{Comment}}
<div style="padding-top:45px;">
<xsl:if test="mdo:isinrole('Administrators')">
<a href="{mdo:jsubmit('@delete-comment', CommentID)}" class="CommandButton">Delete</a>
</xsl:if>
</div>
</div>
<div style="clear:both"/>
</xsl:for-each>
This code display the following for each comment
- Image of commenters. This is very easy since we have a gravatar service. Just take a e-mail and create a commenter image URL: src="http://www.gravatar.com/avatar/{mdo:md5(EMail)}"
- Comment text is displayed as HTML. {h{Comment}} as far as we use a RadEditor we can be sure that HTML does not contain script sections.
- If current user is a portal Administrator it must be able to delete comment. So we have a block of code doing that after <xsl:if test="mdo:isinrole('Administrators')">.
Conclusion
This tutorial shows how to create a simple rating/commenting system with flat comments. In the
live demo we can see a little bit more complex comments with replys. This is not included in the article text, but article attachments contain the real code from xsltdb.com site that supports comment replys. I excluded this function from the article to reduce the article size as it doesn’t demonstrate unique features of Telerik Controls or XsltDb Module.
Using code attached to the article
To use this code you need DotNetBuke 5.2 or higher, 5.4.2 is the recommended version. Deployment is very simple:
- Login as superuser to your DotNetNuke site.
- Install XsltDb module, download it from here
- Create database objects. Go to the Host/SQL tab, paste DatabaseObject.sql into the textbox, select RunAsScript, click Execute.
- Navigate to a page you want to rate/comment
Add new XsltDb module to the page
Click EditXSLT link
Paste ModuleCode.xslt in the textarea
Look through the code and change emails and urls to yours.
Click Update & Publish.
- Enjoy Rating/Commenting.
Tools used to create Comments/Rating module:
- Windows Server 2008 / IIS 7.5 / .NET 3.5
- DotNetNuke 5.4.2 Community Edition with Telerik RadControls inside
- XsltDb DotNetNuke integration module