|   Contact  

ASP.NET WebParts CatalogPart Sample


August 2004
Summary:
This article shows a very simple sample on how to allow users to add WebParts defined in an external file, using Xml.
In this case we will create a class that derives from CatalogPart and reads the WebParts from an Xml file:

 

The most interesing feature of WebParts, and that makes them completely different to any other control is that they allow a user to personalize the content of a page.
This includes not only changing properties on existing WebParts in the page, minimizing and closing them, but also to add completely new WebParts to the page.
The way that WebParts allow end-users to add new WebParts to their page is through a CatalogZone, this type of Zone contains Catalog Parts that inherit from the base class CatalogPart.
By inheriting through this class control developers can create custom functionality that allows them to load the WebParts from anywhere. In this sample we will create one that reads them from Xml, but it would be exactly the same to read them from Sql Server or any database.
Out of the box ASP.NET ships with three CatalogPart's:
  1. PageCatalogPart: This will show all the WebParts in a page that have been closed in the page, allowing end users to "re-add" them to the page.
  2. DeclarativeCatalogPart: This allows page developers to provide a pre-defined set of WebParts that are declared in a template. Note that in the beta2 release there will be a property to specify an external file where you can define all the WebParts.
  3. ImportCatalogPart: This allows end-users to import a .WebPart file that contains the definition of a webpart and allows them to include it in their page.
Visits:

The Code


Lets first show the code.
To run this sample you have to create a new WebSite in Visual Web Developer Express or use Visual Studio .NET 2005 (or just create a virtual directory in IIS).

The Page

Lets start by showing how simple the page is:
Save the following code in a file with the extension .ASPX:
<%@ Page Language="C#" %>
<%@ Register Namespace="MyControls" TagPrefix="My" %>
<html>
<head />
<body>
    
<form id="form1" runat="server">
            
<asp:WebPartManager ID="WebPartManager1" runat="server" />
            <asp:WebPartPageMenu ID="WebPartPageMenu1" runat="server" /><br />
            <asp:WebPartZone ID="WebPartZone1" runat="server">
            
</asp:WebPartZone>
            
<br />
            <asp:CatalogZone runat="server" ID="zone1">
                
<ZoneTemplate>
                    
<My:XmlCatalogPart runat="server" id="xmlCatalogPart1" 
                    datafile="~/parts.xml"
 title="Xml Catalog Part" />
                </ZoneTemplate>
            
</asp:CatalogZone>
    
</form>
</body>
</html>
The first thing to notice is that we are not adding any WebPart to the WebPartZone, the WebParts are only "declared" to be available using a CatalogZone, in this case we will be using the XmlCatalogPart and it will read the available WebParts from the Xml File.
We also added to the page a WebPartPageMenu that will allow us to move between the different display modes of WebParts.

The Xml File


This is the way the xml file (parts.xml) looks like.
The first thing to notice is that I designed this xml to have this structure, it is not mandatory, this is just the way I coded the XmlCatalogPart (shown below). But you could also use roles, or any additional criteria to load and find WebParts.
<?xml version="1.0" encoding="utf-8" ?>
<parts>
    
<part id="1" title="My Lottery WebPart" imageUrl="image1.gif" type="MyWebParts.LotteryPicker" />
    <part id="2" title="My Weather WebPart" imageUrl="image2.gif" type="MyWebParts.WeatherWebPart" />
</parts>

The most important attribute in this Xml is the Type, that essentially allows me to load the WebPart from any assembly or from the Code directory. The rest of the attributes are just to full-fill the WebPartDescription properties.

The CatalogPart

Save the following code into a file named XmlCatalogPart.cs or XmlCatalogPart.vb and save it inside a Code directory inside the virtual directory. You might need to add a reference to System.Desing.dll
C# Code

namespace MyControls {
    using System;
    using System.Web.Caching;
    using System.ComponentModel;
    using System.Collections.Generic;
    using System.Xml;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;

    /// <summary>
    /// Catalog for reading WebParts from an Xml Document
    /// </summary>
    public class XmlCatalogPart : CatalogPart {
        XmlDocument document;
        /// <summary>
        /// Overrides the Title to display Xml Catalog Part by default
        /// </summary>
        public override string Title {
            get {
                string title = base.Title;
                return string.IsNullOrEmpty(title) ? "Xml Catalog Part" : title;
            }
            set {
                base.Title = value;
            }
        }

        /// <summary>
        /// Specifies the Path for the Xml File that contains the declaration of the WebParts, 
        ///     more specifically the WebPartDescriptions
        /// </summary>
        [
        UrlProperty(),
        DefaultValue(""),
        Editor(typeof(System.Web.UI.Design.XmlUrlEditor), typeof(System.Drawing.Design.UITypeEditor)),
        ]
        public string DataFile {
            get {
                object o = ViewState["DataFile"];
                return o == null ? "" : (string)o;
            }
            set {
                ViewState["DataFile"= value;
            }
        }

        /// <summary>
        /// Creates a new instance of the class
        /// </summary>
        public XmlCatalogPart() {

        }

        /// <summary>
        /// Returns the WebPartDescriptions
        /// </summary>
        public override WebPartDescriptionCollection GetAvailableWebPartDescriptions() {
            if (this.DesignMode) {
                return new WebPartDescriptionCollection(new object[] {
                    new WebPartDescription("1""Xml WebPart 1"null),
                    new WebPartDescription("2""Xml WebPart 2"null),
                        new WebPartDescription("3""Xml WebPart 3"null)});
            }

            XmlDocument document = GetDocument();
            List<WebPartDescription> list = new List<WebPartDescription>();
            foreach (XmlElement element in document.SelectNodes("/parts/part")) {
                list.Add(
                    new WebPartDescription(
                        element.GetAttribute("id"),
                        element.GetAttribute("title"),
                        element.GetAttribute("imageUrl")));
            }
            return new WebPartDescriptionCollection(list);
        }

        /// <summary>
        /// Returns a new instance of the WebPart specified by the description
        /// </summary>
        public override WebPart GetWebPart(WebPartDescription description) {
            string typeName = this.GetTypeNameFromXml(description.ID);
            Type type = Type.GetType(typeName);
            return Activator.CreateInstance(type, nullas WebPart;
        }


        /// <summary>
        /// private function to load the document and cache it
        /// </summary>
        private XmlDocument GetDocument() {
            string file = Context.Server.MapPath(this.DataFile);
            string key = "__xmlCatalog:" + file.ToLower();
            XmlDocument document = Context.Cache[key] as XmlDocument;
            if (document == null) {
                using (CacheDependency dependency = new CacheDependency(file)) {
                    document = new XmlDocument();
                    document.Load(file);
                    Context.Cache.Add(key, document, dependency,
                        Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
                }
            }
            return document;
        }

        /// <summary>
        /// Returns the type
        /// </summary>
        private string GetTypeNameFromXml(string webPartID) {
            XmlDocument document = GetDocument();
            XmlElement element = (XmlElement)document.SelectSingleNode("/parts/part[@id='" + webPartID + "']");
            return element.GetAttribute("type");
        }
    }
}
VB.NET Code

Imports System
Imports System.Web.Caching
Imports System.ComponentModel
Imports System.Collections.Generic
Imports System.Xml
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Web.UI.WebControls.WebParts

Namespace MyControls

    ' <summary>
    ' Catalog for reading WebParts from an Xml Document
    ' </summary>
    Public Class XmlCatalogPart
        Inherits CatalogPart

        Private document As XmlDocument

        ' <summary>
        ' Creates a new instance of the class
        ' </summary>
        Public Sub New()
            MyBase.New()
        End Sub

        ' <summary>
        ' Overrides the Title to display Xml Catalog Part by default
        ' </summary>
        Public Overrides Property Title() As String
            Get
                Dim baseTitle As String = MyBase.Title
                Return IIf(String.IsNullOrEmpty(baseTitle), "Xml Catalog Part", baseTitle)
            End Get
            Set(ByVal value As String)
                MyBase.Title = value
            End Set
        End Property

        ' <summary>
        ' Specifies the Path for the Xml File that contains the declaration of the WebParts, 
        '     more specifically the WebPartDescriptions
        ' </summary>
        <UrlProperty(), _
         DefaultValue(""), _
         Editor(GetType(System.Web.UI.Design.XmlUrlEditor), GetType(System.Drawing.Design.UITypeEditor))> _
        Public Property DataFile() As String
            Get
                Dim o As Object = ViewState("DataFile")
                Return IIf(o Is Nothing, "", CStr(o))
            End Get
            Set(ByVal value As String)
                ViewState("DataFile"= value
            End Set
        End Property

        ' <summary>
        ' Returns the WebPartDescriptions
        ' </summary>
        Public Overrides Function GetAvailableWebPartDescriptions() As WebPartDescriptionCollection
            If Me.DesignMode Then
                Return New WebPartDescriptionCollection(New Object() { _
                        New WebPartDescription("1""Xml WebPart 1", Nothing), _
                        New WebPartDescription("2""Xml WebPart 2", Nothing), _
                        New WebPartDescription("3""Xml WebPart 3", Nothing)})
            End If
            Dim document As XmlDocument = Me.GetDocument()
            Dim list As New List(Of WebPartDescription)
            For Each (element As XmlElement in document.SelectNodes("/parts/part"))
                list.Add(New WebPartDescription(element.GetAttribute("id"), _
                                element.GetAttribute("title"), _
                                element.GetAttribute("imageUrl")))
            Next
            Return New WebPartDescriptionCollection(list)
        End Function

        ' <summary>
        ' Returns a new instance of the WebPart specified by the description
        ' </summary>
        Public Overrides Function GetWebPart(ByVal description As WebPartDescription) As WebPart
            Dim typeName As String = Me.GetTypeNameFromXml(description.ID)
            Dim webPartType As Type = System.Type.GetType(typeName)
            Return CType(Activator.CreateInstance(webPartType, Reflection.BindingFlags.Default), WebPart)
        End Function

        ' <summary>
        ' private function to load the document and cache it
        ' </summary>
        Private Function GetDocument() As XmlDocument
            Dim file As String = Context.Server.MapPath(Me.DataFile)
            Dim key As String = ("__xmlCatalog:" + file.ToLower)
            Dim document As XmlDocument = CType(Context.Cache(key), XmlDocument)
            If (document Is Nothing) Then
                Dim dependency As CacheDependency = New CacheDependency(file)
                document = New XmlDocument
                document.Load(file)
                Context.Cache.Add(key, document, dependency, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Normal, Nothing)
            End If
            Return document
        End Function
        ' <summary>
        ' Returns the type
        ' </summary>
        Private Function GetTypeNameFromXml(ByVal webPartID As StringAs String
            Dim document As XmlDocument = GetDocument()
            Dim element As XmlElement = CType(document.SelectSingleNode("/parts/part[@id='" + webPartID + "']"), XmlElement)
            Return element.GetAttribute("type")
        End Function
    End Class
End Namespace
Note. The only code that I do test is the C# Code, the VB.NET Code is a translation from that and might have typo's.


Running the Sample

Browse to the page:
Browse to the Sample Page The first time you run the page you will get a not very interesting empty page, just with the WebPartPage menu in Browse mode like the following image shows:

Set the page in CatalogDisplayMode:
Now, in the drop down menu select the option "Add WebParts to this Page" and click Change
The page will show in Catalog DisplayMode so you can add new webparts to this page. In this case the only webparts that will be shown are the ones listed in the Xml file used by the XmlCatalogPart.

Add the WebParts

Now, Mark both Check Boxes and Click Add. You can see that you can add as many WebParts as you want.
Now you get a much more interesting page that allows you to minimize/restore/close parts of your page.


Summary


This sample, shows how you can use several features in WebParts to create very dynamic applications.

Carlos Aguilar Mares © 2011