Portal Video
Table of Contents
Introduction
As an MVC Framework admirer, I had gone through high and low for Portlet or Webpart solution in ASP.NET MVC, but search didn't come up with desirable solution. This development effort is made to materialize Portlet/Webpart application in ASP.NET MVC Framework. It's indented to provide accumulated view with segregation, and offers personalization features. It's intended to be as concise as possible to focus most on idea, therefore ASP.NET session and application variable shall be our repository, therefore customed or personalized Portlet/Webpart adjustment shall not withstand for multiple ASP.NET Session.
Pre-requisites
In order to follow this article, you need to have some understanding of MVC Framework. If you think you have sufficient expertise, then you are best to further read through this article.
If you still have not setup ASP.NET MVC, please have the items listed below installed before proceeding any further. You can also install ASP.NET MVC using Microsoft Web Platform Installer too.
Data Model
First to begin with, understanding data model is pivotal. Its entity describes the way the Portlet/Webpart would be harmonized into Portal.
Category Entity
It holds categories data, to segregate Portlet/Webpart into categories. Entity has one-many relationship with Portlet
and Portlet_User
entities. Entity would be habitated with data on start application event and resides in ASP.NET application variable.
Portlet Entity
It is a child of Category
entity, has one-many relationship with Portlet_User
entity. It holds default Portlet/Webpart information, regarding their placement on portal.
-
Portlet_ID
: It's an entity's primary key, used to specify portlet.
-
Category_ID
: It's hold category ID which demonstrates which category Portlet/Webpart belongs to
-
Link
: It holds link to RSS Feed
-
Column_No
: Describes column number or webpart zone for the Portlet/Webpart.
-
Title
: Holds RSS Feed title
-
Row Sequence: Holds row number in a particular column
Portlet User Entity
It's a child of Category
, Portlet
and User
entity. It's more or less a replica of Portlet
entity. It holds default Portlet
/Webpart
information, regarding their placement on portal, and habitated at ASP.NET Session start up event. All customization and personalization would be done in this Entity.
Generate Portal View
It comprises several levels of abstraction through ASP.NET usercontrols. Each userControl performs its specific contribution to have overall view.
Portal (Portal.aspx)
It's portal's entry point, and initiates the first level of abstraction, it renders partial view TabPage.asmx.
Tab Page (TabPage.ascx)
It's a first level abstraction. It generates tabpages for individual category which then segregates portlets to their respective tabs. Each Tab constitutes column or webpart zone, here Tab has come up with three columns which are portletColumn1XX
first column or left zone, portletColumn2XX
for second column or middle zone and portletColumn3XX
for third column or right zone. Where XX would be substituted with context category ID. They decide which category the portlet should reside and in which column. Hence portlet placement decision is made at this level.
<%
System.Data.DataRow[] rows = ((WebApplication.Models.ds)Application
["data"]).Category.Select();
System.Data.DataRow[] piRows = rows;
int total_Category_Protlets = 0;
string status_Filter = " Is_Active = " +
ViewData["is_Active_Portlets"].ToString();
int total_Portlets = ((WebApplication.Models.ds)
Session["data"]).Portlet_User.Select(status_Filter).Length;
string funct_Name = "";
string status = "";
if (Convert.ToBoolean(ViewData["is_Active_Portlets"]))
status = " Active ";
else
status = " Disable ";
%>
<%-- application pagetabs are generation --%>
<div id="tabs" style="MIN-HEIGHT: 500px; HEIGHT: 100%">
<strong style="COLOR: black">Total : </strong><em id="currentActivePortlets"
style="COLOR: black">
<%= total_Portlets.ToString()%></em> <strong style="COLOR: black">
<%= status %> RSS Feeds</strong>
<ul>
<% foreach (System.Data.DataRow row in rows)
{ %>
<% total_Category_Protlets =
((WebApplication.Models.ds)Session["data"]).Portlet_User.Select
( status_Filter + " and Category_ID = " +
row["Category_ID"].ToString()).Length; %>
<li><a id="<%= "tab" + row["Category_ID"].ToString() %>"
href="%3C%=%20%22#tabs-%22%20+%20row[%22Category_ID%22].ToString%28%29%20%%3E">
<%= row["Category"].ToString() + "
( " + total_Category_Protlets.ToString() + " ) "%></a>
<%} %>
</li>
</ul>
<%-- application pagetabs contents generation that is portlets/webpart --%>
<% foreach (System.Data.DataRow catRow in rows)
{ %>
<% funct_Name = "catRadButton" + catRow["Category_ID"].ToString(); %>
<script type="text/javascript">
function <%= funct_Name %>() {
if( $('<%= "#radio1-" + catRow["Category_ID"].ToString() %>').
is(':checked') )
{
$('<%= "#" + "tabs-" + catRow
["Category_ID"].ToString() %>' ).find(".portlet").find
(".portlet-content").toggle(true);
$('<%= "#" + "tabs-" + catRow
["Category_ID"].ToString() %>' ).find(".portlet-header
.ui-icon-plusthick").toggleClass("ui-icon-minusthick").
toggleClass("ui-icon-plusthick");
}
else
if( $('<%= "#radio2-" + catRow["Category_ID"].ToString() %>').
is(':checked') )
{
$('<%= "#" + "tabs-" + catRow["Category_ID"].ToString() %>' ).
find(".portlet").find(".portlet-content").toggle(false);
$('<%= "#" + "tabs-" + catRow["Category_ID"].ToString() %>' ).
find(".portlet-header .ui-icon-minusthick").toggleClass
("ui-icon-minusthick").toggleClass("ui-icon-plusthick");
}
else
if( $('<%= "#radio3-" + catRow["Category_ID"].ToString() %>').is
(':checked') )
{
}
}
</script>
<div id="<%= "tabs-" + catRow["Category_ID"].ToString() %>" style="WIDTH: 100px">
<%-- intializing tabpage first column --%>
<%-- intializing tabpage second column --%>
<%-- intializing tabpage third column --%>
<table width="300">
<tbody>
<tr>
<td colspan="3"><input önclick="<%=" funct_name="" %="" type="radio">()
id="<%= "radio1-" + catRow["Category_ID"].ToString() %>"
name='<%= "cat-" + catRow["Category_ID"].ToString() %>' />
<label for="<%= "#radio1-" + catRow["Category_ID"].ToString() %>"
style="color: rgb(92, 135, 178);">Expand</label>
<input önclick="<%=" funct_name="" %="" type="radio">()
id="<%= "radio2-" + catRow["Category_ID"].ToString() %>"
name='<%= "cat-" + catRow["Category_ID"].ToString() %>' />
<label for="<%= "#radio2-" + catRow["Category_ID"].ToString() %>"
style="color: rgb(92, 135, 178);">Collapse</label>
<input önclick="<%=" funct_name="" %="" type="radio">()
id="<%= "radio3-" + catRow["Category_ID"].ToString() %>"
name='<%= "cat-" + catRow["Category_ID"].ToString() %>' checked=checked />
<label for="<%= "#radio3-" + catRow["Category_ID"].ToString() %>" s
tyle="color: rgb(92, 135, 178);">None</label> </td>
</tr>
<tr>
<td valign="top" style="WIDTH: 100px; VERTICAL-ALIGN: 100%">
<%-- intializing value for portlet/webpart that would passed to partial view as
parameter for further assesment --%> <% ViewDataDictionary vdd = new ViewDataDictionary();
vdd["category_ID"] = catRow["Category_ID"].ToString(); %>
<%-- first columns pagetabs portlets/webpart generation --%>s</td>
<td valign="top" style="WIDTH: 150px">
<%-- second columns pagetabs portlets/webpart generation --%>
<div class="column" id="<%= "portletColumn2" + catRow["Category_ID"].ToString() %>"
style="WIDTH: 285px; FONT-SIZE: 1.2em">
<% piRows = ((WebApplication.Models.ds)Session["data"]).Portlet_User.Select
(status_Filter + " and Category_ID = " + catRow["Category_ID"].ToString() +
" and Column_No = 2 ", " Row_Sequence asc ");
foreach (System.Data.DataRow piRow in piRows)
{ vdd["Portlet_ID"] = Convert.ToInt32(piRow["Portlet_ID"]);
vdd["Title"] = piRow["Title"].ToString(); Html.RenderPartial("Portlet", vdd); } %> </div>
</td>
<td valign="top" style="WIDTH: 150px">
<%-- second columns pagetabs portlets/webpart generation --%>
<div class="column" id="<%= "portletColumn3" +
catRow["Category_ID"].ToString() %>" style="WIDTH: 285px; FONT-SIZE: 1.2em">
<% piRows = ((WebApplication.Models.ds)Session["data"]).Portlet_User.Select
(status_Filter + " and Category_ID = " + catRow["Category_ID"].ToString() +
" and Column_No = 3 ", " Row_Sequence asc ");
foreach (System.Data.DataRow piRow in piRows)
{ vdd["Portlet_ID"] = Convert.ToInt32(piRow["Portlet_ID"]);
vdd["Title"] = piRow["Title"].ToString(); Html.RenderPartial("Portlet", vdd); } %> </div>
</td>
</tr>
</tbody>
</table>
</div>
<%} %>
</div>
Portlet (Portlet.ascx)
It's a second level abstraction and rendered only from TabPage.ascx control, it defines Portlet/Webpart frame in which its content needs to reside. It depends on next level abstraction, and relies on AJAX call to portlet controller's content controller method which will flush content inside portlet frame.
<%
string portlet_ID = Convert.ToInt32(ViewData["portlet_ID"]).ToString().Trim();
string portletName = "portlet" + portlet_ID;
string portletFunc = "func" + portletName;
string portletContent = "portlet" + portlet_ID + "Content";
%>
<script type="text/javascript">
function <%= portletFunc + "_" %>() {
<%= portletFunc %>("1");
}
function <%= portletFunc %>(page) {
if( page == 1 )
{
$('#<%= portletContent %>').html('');
}
else
{
$('#<%= portletContent %>').html
('<img alt="Loading, please wait"
src="http://www.codeproject.com/ajax-loader.gif" />');
}
jQuery.ajax({
type:"POST",
url:"Portlet/Content/<%= portlet_ID %>/" + page +"/<%= portletName %>",
success: function(result) {
if(result.isOk == false)
{
$("#<%= portletContent %>").html(result.message);
$("#<%= "Header" + portletName %>").html("");
}
else
{
$("#<%= portletContent %>").html(result);
$("#<%= "Header" + portletName %>").html
("<%= ViewData["Title"] %>");
$(function() {
$("button, input:button, a", ".demo").button();
});
}
},
async: true
});
}
</script>
<%-- portlets/webpart generation --%>
<div class="portlet ui-state-default" id="<%= portletName %>" style="HEIGHT: 2%">
<div class="portlet-header">
<div id="<%= "Header" + portletName %>" style="COLOR: white">Loading..... </div>
</div>
<%-- portlets/webpart content holder--%>
<table width="100%">
<tbody>
<tr>
<td>
<div class="portlet-content" id="<%= portletContent %>">
<img alt="Loading, please wait" src=http://www.codeproject.com/ajax-loader.gif
önload="<%=" />() /> </div>
</td>
</tr>
</tbody>
</table>
</div>
Content Controller Method
It's Portlet controller's controller method, it accepts portlet_ID, page_No
and portletName
. Portlet control invokes it using AJAX call. portlet_ID
parameter is used to specify content source and page_No
to slash down content list view accordance with provided parameters.
public ActionResult Content(int? portlet_ID, int page, string portletName)
{
if( portlet_ID == null )
return View("ErrorPortalItem");
#region declaration
XmlNodeList objNL;
StringBuilder str = new StringBuilder();
int pageSize = Convert.ToInt32
(System.Configuration.ConfigurationManager.AppSettings
["PageSize"].ToString());
string title = "";
string ItemLink = "";
string ItemTitle = "";
int total_Items = 0;
int last_Item_No = 0;
#endregion
#region loading RSS feed
string link = ((WebApplication.Models.ds)
this.HttpContext.Application["data"]).Portlet.FindByPortlet_ID
( Convert.ToInt32( portlet_ID ) ).Link;
XmlDocument objDoc = new XmlDocument();
try
{
objDoc.Load(link);
}
catch
{
return View("ErrorPortalItem");
}
#endregion
#region parsing RSS parent node
objNL = objDoc.SelectNodes("rss/channel");
if (null != objNL)
{
title = objNL[0].ChildNodes[0].InnerText;
}
#endregion
#region parsing items in RSS feed
if (null != objDoc)
{
objNL = objDoc.SelectNodes("rss/channel/item");
if (null != objNL)
{
int counter = 1;
string description = "";
total_Items = objNL.Count;
foreach (XmlNode XNode in objNL)
{
if (counter >= ((page * pageSize) - pageSize) &&
counter <= (page * pageSize))
{
str.Append("<li>");
ItemTitle = "";
description = "";
foreach (XmlNode XNodeNested in XNode.ChildNodes)
{
switch (XNodeNested.Name)
{
case "description":
description = XNodeNested.InnerText;
break;
case "title":
ItemTitle = XNodeNested.InnerText;
break;
case "link":
ItemLink = XNodeNested.InnerText;
break;
}
}
str.Append("</a></li><a>");
last_Item_No = counter;
}
counter++;
}
}
}
#endregion
#region setting view naviation variables
double temp = total_Items / pageSize;
int possible_Pages = Convert.ToInt32(Math.Ceiling(temp));
ViewData["Next_Page"] = page + 1;
ViewData["Previous_Page"] = page - 1;
if ( ( page + 1 ) > possible_Pages)
ViewData["Is_Next_Page_Possible"] = false;
else
ViewData["Is_Next_Page_Possible"] = true;
if ( ( page ) == 1)
ViewData["Is_Previous_Page_Possible"] = false;
else
ViewData["Is_Previous_Page_Possible"] = true;
#endregion
#region setting views content variables
ViewData["content"] = str.ToString();
ViewData["title"] = title;
ViewData["portletName"] = portletName;
#endregion
return View();
}
Detail View
When content list item's description is outsized it constraint size or it contain html document, then it need to be display in Dialog. Detail button is provided to view detail which would invoke JavaScript OpenDialog
function.
<%
string funcItem = "func" + ViewData["portletName"].ToString();
string previous_Function = funcItem +
"(" + ViewData["Previous_Page"].ToString() + ")";
string next_Function = funcItem + "(" + ViewData["Next_Page"].ToString() + ")";
%>
<table width="100%">
<tbody>
<tr>
<td><%= ViewData["Header"].ToString() %> <%-- RSS feed content holder --%>
<%= ViewData["content"].ToString()%> </td>
</tr>
<tr>
<td><%-- content navigation--%> <%--
<div class="demo">--%> <%--
<p id="<%= "PortletContentLoading" + ViewData["item"].ToString() %>">
<img style="width:640px; height:480px" alt="Loading, please wait"
src="http://www.codeproject.com/ajax-loader.gif" /> --%>
<% if (Convert.ToBoolean(ViewData["Is_Previous_Page_Possible"])) {%>
<input style="padding: 0em;" class="demo ui-button ui-widget ui-state-default
ui-corner-all"
value=" < " önclick="<%=" previous_function="" %="" type="button"> />
<%}%> <% if (Convert.ToBoolean(ViewData["Is_Next_Page_Possible"])) {%>
<input style="padding: 0em;" class="demo ui-button ui-widget ui-state-default
ui-corner-all" value=" >
" önclick="<%=" next_function="" %="" type="button"> /> <%}%> <%-- </p>
</div>
--%> </td>
</tr>
</tbody>
</table>
On calling the method get AJAX request is generated to Portlet controller's GetItemDetail
controller method. It consumes portlet_ID
and item_No
to get specific portlet's specific item's description. Method will return required description of an Item to be displayed in dialog.
public ActionResult GetItemDetail(int? portlet_ID, int? item_No)
{
if (portlet_ID == null || item_No == null)
return View("ErrorPortalItem");
#region declaration
XmlNodeList objNL;
StringBuilder str = new StringBuilder();
int pageSize = Convert.ToInt32
(System.Configuration.ConfigurationManager.AppSettings
["PageSize"].ToString());
string title = "";
string imagePath = "";
string ItemName = "Item" + portlet_ID.ToString();
string ItemLink = "";
string ItemTitle = "";
#endregion
#region loading RSS path
string link = ((WebApplication.Models.ds)
this.HttpContext.Application["data"]).Portlet.FindByPortlet_ID
(Convert.ToInt32(portlet_ID)).Link;
XmlDocument objDoc = new XmlDocument();
try
{
objDoc.Load(link);
}
catch
{
return View("ErrorPortalItem");
}
#endregion
#region parsing RSS header for title
objNL = objDoc.SelectNodes("rss/channel");
if (null != objNL)
{
title = objNL[0].ChildNodes[0].InnerText;
}
#endregion
#region get image path for portlet from RSS feed
objNL = objDoc.SelectNodes("rss/channel/image");
if (null != objNL)
{
objNL = objDoc.SelectNodes("rss/channel/image/url");
if (objNL.Count != 0)
imagePath = objNL[0].InnerText;
else
imagePath = "";
}
#endregion
#region content to be generated from RSS feed
int total_Items = 0;
if (null != objDoc)
{
objNL = objDoc.SelectNodes("rss/channel/item");
if (null != objNL)
{
int counter = 1;
string description = "";
total_Items = objNL.Count;
foreach (XmlNode XNode in objNL)
{
if (counter == item_No)
{
ItemTitle = "";
description = "";
foreach (XmlNode XNodeNested in XNode.ChildNodes)
{
switch (XNodeNested.Name)
{
case "description":
description = XNodeNested.InnerText;
break;
case "title":
ItemTitle = XNodeNested.InnerText;
break;
case "link":
ItemLink = XNodeNested.InnerText;
break;
}
}
break;
}
counter++;
}
str.Append(description);
}
}
#endregion
#region setting view content variables
ViewData["detail"] = str.ToString();
ViewData["imagePath"] = imagePath;
ViewData["title"] = title;
#endregion
return View("ItemDetail");
}
Customization through Drag and Drop
Drag and drop personalization is accomplished through JQuery Library, which provides portlet placement and category change feature.
Droppable functionality
When user wants to change Portlet/Webpart category, he needs to drag and drop particular Portlet/Webpart to that specific category tab. To implement this functionality, jquery.ui.draggable.js is inducted in the solution. When Portlet/Webpart is dragged and dropped onto tab associated droppable method is called, during that mean time PortletsPlacementManager
method is called which performs placement settlement in repository.
var current_Column_ID = "";
var portlet_Header_Icon;
var portlet_Item_Name = "";
$(function() {
var $tabs = $("#tabs").tabs();
var $tab_items = $("ul:first li",$tabs).droppable({
accept: '.column div' ,
tolerance: 'pointer',
opacity: .50,
containment: '.container',
cursor: 'move',
cursorAt: { cursor: 'move', top: -155, left: -55, bottom: 0 },
drop: function(ev, ui) {
var $item = $(this);
var $list = $($item.find('a').attr
('href')).find('.column')[0];
PortletsPlacementManager($($item.find('a').attr('href')).find
('.column').attr("id"),current_Column_ID,portlet_Item_Name,0,1);
ui.draggable.hide('slow', function() {
$tabs.tabs('select', $tab_items.index
($item));
$(this).appendTo($list).show('slow');
});
$(this).find(".portlet-content").toggle(true);
}
});
});
Placement Management
Whenever Portlet/Webpart customization took place, PortletsPlacementManager
JavaScript method would be called to synchronize portlet's placement adjustment with repository.
function PortletsPlacementManager(column_ID,sender_Column_ID,
portlet_ID,row_No,is_Drop) {
jQuery.ajax({
type:"POST",
url:"Portlet/PortletsPlacementManager/" + column_ID + "/" +
portlet_ID +"/" + row_No + "/" + is_Drop ,
success: function(result) {
if(result.isOk != false)
{
if( is_Drop = 1 )
{
TotalCatetogoryPortlets(column_ID);
TotalCatetogoryPortlets(sender_Column_ID);
}
}
},
async: true
});
}
Calculate Current Category Portlet
Whenever portlet/Webpart category changes, portlet in specific category changes as well, to have synchronize view with repository, category from where portlet is transferred and where its transfer required reevaluation it's portlets count, for that purpose TotalCatetogoryPortlets
method is called, it take column id (category id is resided within it) and returns current total number of portlets assigned to that category.
function TotalCatetogoryPortlets(column_ID) {
jQuery.ajax({
type:"POST",
url:"Portlet/TotalCatetogoryPortlets/" + column_ID,
success: function(result) {
if(result.isOk != false)
{
var name = "#tab" + column_ID.substring(4);
$(name).html( result );
}
},
async: true
});
}
Managing Portlets Status
When uses want to remove or disable Portlet/Webpart from his view, he just needs to cancel that particular Portlet/Webpart. By canceling it, that Portlet/Webpart shall have disabled status. To reactivate it, user needs to go to Disabled Portlet list and just click restore button at top of header.