Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Webpart/Portlet development in ASP.NET MVC Framework

0.00/5 (No votes)
17 May 2014 2  
Solution adorned with drag and drop Portlet/Webpart customization feature in ASP.NET MVC Framework. It summoned JQuery for better user experience evading inclusive page refresh during personalization.

webpartsMVC/VideoImage.jpg
Portal Video

Application's Live Demo

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.

webpartsMVC/Data_Model.JPG
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.

webpartsMVC/Portal_Loading.jpg

webpartsMVC/Portal_Loaded.jpg

webpartsMVC/collapse.jpg

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)
        {
            // invalid portlet id
            if( portlet_ID == null )
                return View("ErrorPortalItem");

            #region declaration
            XmlNodeList objNL;
            StringBuilder str = new StringBuilder();
            // portlet page size 
            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
            {
                // invalid portlet id
                return View("ErrorPortalItem");
            }
            #endregion

             #region parsing RSS parent node
            objNL = objDoc.SelectNodes("rss/channel");
            if (null != objNL)
            {
                // get title for portlet
                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)
        {
            // invalid portlet id
            if (portlet_ID == null || item_No == null)
                return View("ErrorPortalItem");
           
            #region declaration
            XmlNodeList objNL;
            StringBuilder str = new StringBuilder();
            
            // portlet page size 
            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
            {
                // invalid portlet id
                return View("ErrorPortalItem");
            }
            #endregion
           
            #region parsing RSS header for title
            objNL = objDoc.SelectNodes("rss/channel");
            if (null != objNL)
            {
                // get title for portlet
                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.

webpartsMVC/Drag_With_In_Tab.jpg

webpartsMVC/TabDrop.jpg

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.

webpartsMVC/Disabled_Portlets.jpg

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here