Create a “Portal-in-a-Box” - 30 Oct 2009

Using ASP.NET 2.0 Web Parts and Composite Controls to Make a Web-ready Portal

asp:Feature

 

Create a Portal-in-a-Box

Using ASP.NET 2.0 Web Parts and Composite Controls to Make a Web-ready Portal

 

By Matt Dinovo

 

In the world of Web portals sites where a user can customize pages for their own content and layout preferences many different paradigms exist. Microsoft s SharePoint paradigm of what a Web part is and how to manipulate them on a page is what ASP.NET 2.0 s Web part architecture is based upon. This architecture can be a powerful tool in many business scenarios where different audiences need to view different content via a single application. However, the standard ASP.NET 2.0 architecture has several limitations in its implementation that can be frustrating to a developer new to the technology.

 

In addition, most developers want to make portals that operate like Live.com or My Yahoo, not SharePoint. They want users to be able to create multiple pages as part of their customizations, as well as to be able to choose from multiple layouts. The ASP.NET Web part architecture does not natively allow for such features. In addition, the strong tie to the SqlPersonalizationProvider, limited documentation on PersonalizationState, and the need for several controls to work in conjunction to make a portal page functional make creating a well designed (and nice looking) portal site complex. But it doesn t have to be like this! In this article, I ll show you how to develop a portal in a box : a single ASP.NET 2.0 server control that, when dropped on a page, can support any number of tabs of various configurations. This architecture is one that allows developers to consolidate all the plumbing into a single block that one simply drops onto a page for it to function.

 

Creating a Personalization Provider

Because in our new portal a user can not only configure the Web parts on a page (which we ll call a desktop), but also add/remove/delete individual Tab Pages on this desktop, we must first create a personalization provider that supports this. Only one personalization provider is included in the .NET Framework, the SqlPersonalizationProvider. The intention is to use this in conjunction with ASP.NET s membership architecture, intrinsically tied to SQL Server 2005. However, this implementation is rigid in that it only supports a single personalization per ASPX page. To the SqlPersonalizationProvider, default.aspx?tab=1 is the same as default.aspx?tab=2. We want to be able to have an infinite number of tabs per page, so we must change the behavior of the personalization provider. First, I created simple XML serialized entity classes to be our database it will save an XML file to the root of our Web application that contains the personalization information. I did this to show what is actually being stored your personalization provider can save this to any database you would like. (In the code samples that accompany this article, the Desktop class contains all the serialization code for this demo; see end of article for download details). In this demo, we re going to store the actual personalization information as a byte array similar to the way the SqlPersonalizationProvider class stores this information in SQL Server. Therefore, to accomplish our goal of being able to store multiple personalizations per ASPX page, we re going to create a PersonalizationProvider like the one shown in Figure 1.

 

Protected Overrides Sub LoadPersonalizationBlobs( _

   ByVal webPartManager As WebPartManager, _

   ByVal path As String, ByVal userName As String, _

   ByRef sharedDataBlob() As Byte, ByRef userDataBlob() As Byte)

   Dim dt As Desktop = Desktop.Load(userName.Replace("\"c, "_"c))

   If Not dt Is Nothing Then

       Dim tp As TabPage = dt.Tabs.FindByID(GetPageID())

       If Not tp Is Nothing Then

           userDataBlob = tp.Personalization

       End If

   End If

End Sub

Protected Overrides Sub ResetPersonalizationBlob( _

   ByVal webPartManager As WebPartManager, _

   ByVal path As String, ByVal userName As String)

   Dim usr As String = userName.Replace("\"c, "_"c)

   Dim dt As Desktop = Desktop.Load(userName)

   If Not dt Is Nothing Then

       Dim tp As TabPage = dt.Tabs.FindByID(GetPageID())

       If Not tp Is Nothing Then

           tp.Personalization = Nothing

           dt.Save(userName)

       End If

   End If

End Sub

Protected Overrides Sub SavePersonalizationBlob( _

   ByVal webPartManager As WebPartManager, _

   ByVal path As String, ByVal userName As String, _

   ByVal dataBlob() As Byte)

   Dim usr As String = userName.Replace("\"c, "_"c)

   Dim dt As Desktop = Desktop.Load(usr)

   If dt Is Nothing Then dt = New Desktop()

   Dim tp As TabPage = dt.Tabs.FindByID(GetPageID())

   If (Not tp Is Nothing) Then

       tp.Personalization = dataBlob

   Else

       tp = New TabPage()

       tp.PageId = GetPageID()

       tp.Name = "Home"

       tp.Personalization = dataBlob

       dt.Tabs.Add(tp)

   End If

   dt.Save(usr)

End Sub

Figure 1: Create a PersonalizationProvider like this one.

 

It is important to note that, in this code sample, we re ignoring the path parameter and using a method call to GetPageID, which returns the current page s tab ID or generates a new one (see Figure 2). This is how we get around the base provider s behavior of only acknowledging the physical page and not taking into account query string information.

 

Private Shared Function GetPageID() As Guid

   Dim ctx As HttpContext = HttpContext.Current

   Dim userName As String = _

       ctx.User.Identity.Name.Replace("\"c, "_"c)

   If Not ctx.Request.QueryString("tab") Is Nothing Then

       Return New Guid(ctx.Request.QueryString("tab"))

   Else

       Dim dt As Desktop = Desktop.Load(userName)

       If Not dt Is Nothing Then

           If dt.Tabs.Count > 0 Then

               Return dt.Tabs(0).PageId

           End If

       End If

   End If

   Return Guid.NewGuid()

End Function

Figure 2: Ignore the path parameter and use a method call to GetPageID, which returns the current page s tab ID or generates a new one.

 

Finally, to use this provider instead of the default, you must configure your Web site by editing the personalization section in the web.config file (see Figure 3). That s all there is to it. With this in place, all the personalization functions will persist to an XML file with the currently logged in user s name. You could stop here, use the included functionality of the ASP.NET Web parts, and be able to have a fully functional portal. However, let s see how to encapsulate the core portal functionality into a single control.

 

 

   

     

       

       

     

   

 

Figure 3: Configure your Web site by editing the personalization section in the web.config file.

 

Creating the Portal Control

To create a rich portal experience, several server controls are required to work in conjunction for everything to function. At a minimum, you must have a WebPartManager that is responsible for all the operations of the portal (e.g., adding/removing Web parts, etc.), as well as one or more WebPartZones where the Web parts will reside. Web parts can be declaratively listed in a WebPartZone s ZoneTemplate; however, there is one major drawback to this: declaratively listed Web parts are permanently part of the page, meaning a user can only close, but not delete, them. Although this does not sound too catastrophic, closed Web parts remain part of the personalization blob and part of the page s control tree. Most users intend to remove a Web part permanently when they close it, so having many hidden Web parts adding overhead is something you should try to avoid if at all possible.

 

So, if we don t want to declaratively list out the Web parts as part of the WebPartZone, we must add a CatalogZone to the mix. If you want to edit existing Web parts, such as changing the title or border, or even manipulating custom properties, you ll need to add an EditorZone. To muddy the waters further, both the CatalogZone and EditorZone can contain multiple zone parts that perform a specific function. For example, in a CatalogZone, you could place a PageCatalogPart that lists all the hidden controls on a page and a DeclarativeCatalogPart that wraps standard ASP.NET server controls in a GenericWebPart fa ade for use in the Web part architecture. There are many more, but I won t dwell on the specifics here.

 

Obviously, there are quite a few pieces of the portal puzzle that you must keep track of. You can see how a developer might get confused as to what to use when. To eliminate some of this confusion, and to help further the implementation of our Web-ready portal, let s combine the required functionality into a single composite control. First, we must create our own CatalogZone and EditorZone that we can programmatically add to our control. The base CatalogZone and EditorZone implement their parts as templates declaratively in the page. However, by creating our own zones that inherit from CatalogZoneBase and EditorZoneBase, we can determine which parts appear at run time (or at the very least, programmatically). In implementations of CatalogZone and EditorZone, you must define a part collection containing all the catalog or editor parts that appear in a zone. In our example, we are declaring a static collection, but you could see how you could add additional parts in different scenarios. To illustrate this, I added a Boolean property onto our custom CatalogZone to indicate whether to display a PageCatalogPart. These implementations look like the examples in Figure 4 and Figure 5.

 

Public NotInheritable Class MyCatalogZone

   Inherits CatalogZoneBase

   Private _pageCatalog As Boolean

   Public Property PageCatalog() As Boolean

       Get

           Return _pageCatalog

       End Get

       Set(ByVal value As Boolean)

           _pageCatalog = value

       End Set

   End Property

   Friend Sub New()

       'Internal Constructor

   End Sub

   Protected Overrides Function CreateCatalogParts() _

     As CatalogPartCollection

       Dim parts As New List(Of CatalogPart)

       Dim catalog As New MyCatalogPart()

       catalog.ID = "sample_catalog"

       parts.Add(catalog)

       If _pageCatalog Then

           Dim pageCatalog As New PageCatalogPart()

           pageCatalog.ID = "page_catalog"

           parts.Add(pageCatalog)

       End If

       Return New CatalogPartCollection(parts)

   End Function

End Class

Figure 4: Add a Boolean property onto our custom CatalogZone to indicate whether to display a PageCatalogPart.

 

Public NotInheritable Class MyEditorZone

   Inherits EditorZoneBase

   Protected Overrides Function CreateEditorParts() As EditorPartCollection

       Dim parts = New List(Of EditorPart)()

       Dim aep As New AppearanceEditorPart()

       aep.ID = "appearance_editor"

       parts.Add(aep)

       Return New EditorPartCollection(parts)

   End Function

End Class

Figure 5: Add a Boolean property onto our custom CatalogZone to indicate whether to display a PageCatalogPart.

 

Note in Figure 4 the use of the MyCatalogPart class. This is another custom class that allows us to define programmatically which parts appear in a catalog. You may want to do this for a variety of reasons; for example, perhaps you wish to display a certain set of parts based on a user s role. The use of a custom CatalogPart allows us to accomplish this.

 

As you can see from Figure 6, the CatalogPart s primary function is to present a WebPartDescriptionCollection to the user so they can select the Web part(s) to add to a zone. When a user selects a WebPart from the catalog, the GetWebPart method is called, which actually returns the Web part. In this example (and in the downloadable code), I created a simple Web part that wraps a standard ASP.NET calendar control.

 

Public Class MyCatalogPart

   Inherits CatalogPart

   Public Sub New()

       MyBase.Title = "Custom Parts"

       MyBase.Description = "Lorem ipsum dolor sit."

   End Sub

   Public Overrides Function GetAvailableWebPartDescriptions() _

    As WebPartDescriptionCollection

       Dim descriptions As New List(Of WebPartDescription)()

       descriptions.Add(New WebPartDescription(_

     New MyWebParts.CalendarPart()))

       Return New WebPartDescriptionCollection(descriptions)

   End Function

   Public Overrides Function GetWebPart(ByVal description _

     As WebPartDescription) As WebPart

       Select Case description.ID

           Case MyWebParts.CalendarPart.PARTID

               Return New MyWebParts.CalendarPart()

           Case Else

               Return Nothing

       End Select

   End Function

End Class

Figure 6: The CatalogPart s primary function is to present a WebPartDescriptionCollection to the user so they can select the Web part(s) to add to a zone.

 

Therefore, even after all of this plumbing, we have not yet actually created the unified portal control. To do this, we are going to use one of the new classes in the 2.0 framework, CompositeControl. The CompositeControl simplifies creation of a WebControl with the INamingContainer interface, and is designed to be used when the server control is simply a container for child controls. In this example, we are going to lay out our portal by overriding the CreateChildControls method, as shown in Figure 7.

 

Public Class MyPortal

   Inherits CompositeControl

   Private _template As Control

   Private _wpm As New WebPartManager()

   Protected Overrides Sub CreateChildControls()

       If (Not DesignMode) Then

           _wpm.ID = "wpm"

           Dim cz = New MyCatalogZone()

           cz.ID = "cz"

           Dim ez = New MyEditorZone()

           ez.ID = "ez"

           Me.Controls.Add(_wpm)

           Me.Controls.Add(cz)

           Me.Controls.Add(ez)

           If Not _template Is Nothing Then Me.Controls.Add(_template)

       End If

       MyBase.CreateChildControls()

   End Sub

End Class

Figure 7: Override the CreateChildControls method.

 

Looking at this, you can see that we are simply adding a WebPartManager, our CatalogZone, and our EditorZone to the control collection of the composite control. One might ask, What about the WebPartZones? We want the user to be able to choose from multiple templates or layouts of WebPartZones, so we need to dynamically load the selected template from personalization and load that as part of this control. To accomplish this, we ll override the OnInit method, as shown in Figure 8.

 

Protected Overrides Sub OnInit(ByVal e As EventArgs)

   If Not DesignMode Then

       Dim dt As Desktop = _

          Desktop.Load(Me.Page.User.Identity.Name.Replace("\"c, "_"c))

       If Not dt Is Nothing Then

           If Not Me.Page.Request.QueryString("tab") Is Nothing Then

               Dim tp As TabPage = _

                  dt.Tabs.FindByID(New Guid(Me.Page.Request.QueryString("tab")))

               If Not tp Is Nothing AndAlso _

                  Not String.IsNullOrEmpty(tp.TemplateUrl) Then

                   _template = Me.Page.LoadControl(tp.TemplateUrl)

               End If

           End If

       End If

       If _template Is Nothing Then

           _template = Me.Page.LoadControl(Me._defaultTemplate)

       End If

       Me.EnsureChildControls()

   End If

   MyBase.OnInit(e)

End Sub

Figure 8: Override the OnInit method.

 

Now we are able to retrieve the virtual path to a user control containing the WebPartZones and dynamically add that to the control tree based on the identifier in the tab query string. One item to particularly note is that one can only add WebPartZones programmatically in the Init event. If you were to add a WebPartZone any later in the event cycle ASP.NET will throw an exception.

 

The Finished Product

So what did all of that buy us? Let s look at a screenshot of the finished product (see Figure 9; to illustrate things a bit better, I added images to the zone template of the Web part zones). Here you see the Web parts displayed in a single column.

 


Figure 9: The finished product.

 

The Display Mode dropdown list controls the Web part manager (which is part of the CompositeControl) and changes the display mode among Browse, Catalog, and Edit. When one changes the page to Catalog mode (where the list of available Web parts are displayed), you can see the custom catalog zone complete with the dummy calendar Web part created earlier (see Figure 10).

 


Figure 10: The custom catalog zone complete with the dummy calendar Web part created earlier.

 

To add a new tab, we can enter the name into the textbox and select a template to use. The values of the template dropdown are the virtual path to available user controls containing the actual Web part zones. When I do this, you can see a second tab appear with a two-column configuration, as in Figure 11.

 


Figure 11: The benefits of the template dropdown.

 

As you can see, we have the same page using a query string tab to indicate which personalized page to show. You can navigate between Home and New by changing the Guid identifying the tab.

 

Conclusion

The ASP.NET 2.0 Web part architecture is a flexible and powerful infrastructure that allows for customized displays of information. While its implementation can be somewhat tricky, the ability to encapsulate much of the functionality into a single control helps developers leverage this technology while not having to worry about the inner workings. Best of all, one can extend this design pattern in many different directions with little effort. For example, one could incorporate portal navigation and management (for example, add/delete/rename functions) directly into the composite control, as well. Alternatively, you might use a technology like Microsoft s Atlas framework to eliminate the postbacks that occur when changing modes or manipulating Web parts. I hope this article demystifies some of the building blocks of the Web part architecture and encourages you to go out and try some portal development of your own.

 

The source code accompanying this article is available for download.

 

Matt Dinovo is a Solution Developer with Avanade (http://www.avanade.com). He has been developing Web applications based on Microsoft technology since 1995 for companies like Intel, Compuware, Ernst & Young, and Compaq. He lives in Ohio with his wife and two children. You can reach him via his blog at http://mattdinovo.spaces.live.com.

 

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish