Chapter 4. Working with Web Parts

SharePoint is a fascinating platform because it comprises so many technologies and subsystems, from site-definition templates to the object model, XML and XSLT, .NET and HTML, and everything in between.

Of all these, developing for the web-part framework is my favorite. The idea that you can create discrete bits of applications and reassemble them as needed is extremely powerful—and in fact is what object-oriented programming is all about. Web parts are the embodiment of that powerful idea in a web UI. You can build components (web parts) to meet a very specific requirement, confident that you'll be able to reuse them and even share data between them at a later time to meet future requirements as well.

With the release of SharePoint 2007—Windows SharePoint Services 3.0 (WSS 3.0) and Microsoft Office SharePoint Server (MOSS)—your web parts will be based on the ASP.NET 2.0 web-part framework rather than the SharePoint-specific framework in SharePoint 2003. This means that you can create and test web parts by using pure ASP.NET if you want. And if your web parts won't need access to the SharePoint object model, you can develop them on a computer that is not a SharePoint server, adding more flexibility to your development environment.

For those rare cases when you need some web-part feature that Microsoft has deprecated in the new framework, you can still instantiate a SharePoint web-part class. In SharePoint 2007, this class is simply a wrapper that maps the old 2003 web-part properties and methods onto the new ASP.NET 2.0 web-part class. But, for the most part, you'll want to stick with ASP.NET 2.0 web parts for flexibility and forward compatibility.

I need to say a few words about deploying the web parts you'll create by using the following recipes. There are several ways to deploy web parts, and here I use what I find the simplest for development purposes, which is to manually copy the web part's signed assembly (.dll) to the Global Assembly Cache (GAC), manually add the corresponding <SafeControl> element to SharePoint's Web.config file, and then use a site collection's web-part gallery pages to add the web part. However, when you're ready to move your web parts into production, you'll want to create a solution package to deploy it to your SharePoint farm. Creating a solution is, unfortunately, more work than it ought to be. But Microsoft has created an add-in called Windows SharePoint Services 3.0 Tools: Visual Studio 2005 Extensions, Version 1.1, which as of this writing can be found at www.microsoft.com/downloads/details.aspx?FamilyID=3E1DCCCD-1CCA-433A-BB4D-97B96BF7AB63&displaylang=en. Among the many features this add-on includes is a template for creating web parts that will make debugging and deploying your work much easier.

A few other things about web-part development to keep in mind before we begin:

  • How you configure security and permissions in SharePoint, along with user permissions, will affect what operations can be performed by a web part at runtime, because web parts will run under the permissions of the current user unless you explicitly override those credentials.

  • Use Try/Catch statements freely, because if your web part throws an unhandled error during runtime in production, SharePoint will provide virtually no information about the error, and the site administrator's only option may be to remove the web part from the page.

  • Web parts must always be signed if they'll be placed in the GAC, and should always be signed anyway. There are arguments for and against GAC deployment. The argument against is that assemblies in the GAC are fully trusted, so if you place a web part there, it can do more harm than one deployed to SharePoint's in folder. The argument for GAC deployment is that it will make your life easier. If you are the source of all web parts in your production environment, go ahead and place them in the GAC. If you receive web parts from other sources (either commercial or separate development groups), you may want to use a in deployment.

  • All web-part recipes developed here assume that SharePoint's security trust level is set to Full. If you are in charge of the SharePoint server and trust the quality and dependability of all code, setting trust to Full is not unreasonable. However, if you receive many web parts from other sources, you may want to use SharePoint's support for Code Access Security (CAS). You can find more on CAS in the SharePoint SDK.

Ultimately, the best way to learn any programming task is to roll up your sleeves and dig in. I'm confident that you'll find the following recipes both useful and instructive. Let's get to work!

Creating a Simple RSS Feed Web Part

If you're new to creating SharePoint web parts, this is a great place to start. This web part will enable you to display an RSS source to any page, converting the underlying XML of the RSS feed into legible Hypertext Markup Language (HTML).

Note

A simple RSS Feed web part exists out of the box in MOSS, but this recipe will enable you to have an RSS Feed web part for WSS 3.0 environments and enable you to expand on the functionality to suit your needs.

Recipe Type: Web Part

Ingredients

Assembly References

  • Windows SharePoint Services assembly

  • System.Web .NET assembly

Class Library References

  • System.Web

  • System.Web.UI

  • System.Web.UI.WebControls

  • System.Web.UI.WebControls.WebParts

  • System.Web.UI.HtmlControls

Special Considerations

  • SharePoint 2007 web parts are simply ASP.NET 2.0 web parts designed for use in SharePoint. This was not the case with SharePoint 2003, which had web-part classes that were tightly tied to the SharePoint object model. The SharePoint 2007 object model still provides its own web-part classes for backward compatibility, but for most purposes you're better off using the generic ASP.NET web-part classes.

  • The deployment instructions given in the "To Run" section will activate the web part for only a single site collection. The preferred method for deploying a web part for an entire SharePoint web application or web farm is through a SharePoint solution.

  • One of the more interesting techniques shown in this recipe is that of reading a web page programmatically. We use this technique to get the RSS page Extensible Markup Language (XML), but can just as easily read any http: source in the same way. In this example, I'm calling an RSS feed that does not require authentication, and so I simply attach my default credentials. If I were reading a secure RSS feed that required authentication, I could attach a specific credential by using the System.Net.NetworkCredential() method.

Preparation

  1. Create a new C# or Visual Basic .NET (VB.NET) class library.

  2. Add references to the System.Web and Windows.SharePoint.Services .NET assemblies.

  3. On the project properties Signing tab, select the Sign the Assembly checkbox and specify a new strong-name key file.

Process Flow

Process Flow
  1. Ensure that the Url custom property of our RSS web part has been filled in. If not, display a message informing the user that it's required.

  2. Read the RSS XML into an ADO DataSet object.

  3. If an error occurred while reading the RSS source, display the resulting error to the web-part page.

  4. Loop through each article returned in the RSS XML.

  5. Write the title, description, and link to the page.

Recipe—VB (See Project RSSWebPartVB, Class RSSWebPartVB.vb)

Imports System
Imports System.Web
Imports System.Web.Security
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Web.UI.WebControls.WebParts
Imports System.Web.UI.HtmlControls
Imports System.Xml
Imports System.Data
Namespace RSSWebPartVB
    Public Class RSSWebPart
        Inherits WebPart
        ' Local variables to hold web-part
        ' property values
        Private _url As String
        Private _newPage As Boolean = True
        Private _showDescription As Boolean = True
        Private _showUrl As Boolean = True
        ' Property to determine whether article should
        ' be opened in same or new page
        <Personalizable()> _
        <WebBrowsable()> _
        Public Property NewPage() As Boolean
            Get
                Return _newPage
            End Get
            Set(ByVal value As Boolean)
                _newPage = value
            End Set
        End Property
        ' Should Description be displayed?
        <Personalizable()> _
        <WebBrowsable()> _
        Public Property ShowDescription() As Boolean
            Get
                Return _showDescription
            End Get
            Set(ByVal value As Boolean)
                _showDescription = value
            End Set
        End Property
' Should URL be displayed?
        <Personalizable()> _
        <WebBrowsable()> _
        Public Property ShowUrl() As Boolean
            Get
                Return _showUrl
            End Get
            Set(ByVal value As Boolean)
                _showUrl = value
            End Set
        End Property
        ' Property to set URL of RSS feed
        <Personalizable()> _
        <WebBrowsable()> _
        Public Property Url() As String
            Get
                Return _url
            End Get
            Set(ByVal value As String)
                _url = value
            End Set
        End Property
        ' This is where the HTML gets rendered to the
        ' web-part page.
        Protected Overloads Overrides Sub RenderContents( _
           ByVal writer As HtmlTextWriter)
            MyBase.RenderContents(writer)
            ' Step 1: Ensure Url property has been provided
            If Url <> "" Then
                ' Display heading with RSS location URL
                If ShowUrl Then
                    writer.WriteLine("<hr/>")
                    writer.WriteLine("<span style='font-size: larger;'>")
                    writer.WriteLine("Results for: ")
                    writer.WriteLine("<strong>")
                    writer.WriteLine(Url)
                    writer.WriteLine("</strong>")
                    writer.WriteLine("</span>")
                    writer.WriteLine("<hr/>")
                End If
                displayRSSFeed(writer)
            Else
                ' Tell user they need to fill in the Url property
                writer.WriteLine( _
                   "<font color='red'>RSS Url cannot be blank</font>")
            End If
        End Sub
Private Sub displayRSSFeed(ByVal writer As HtmlTextWriter)
            Try
                ' Step 2: Read the RSS feed into memory
                Dim wReq As System.Net.WebRequest
                wReq = System.Net.WebRequest.Create(Url)
                wReq.Credentials = _
                   System.Net.CredentialCache.DefaultCredentials
                ' Return the response.
                Dim wResp As System.Net.WebResponse = wReq.GetResponse()
                Dim respStream As System.IO.Stream = _
                   wResp.GetResponseStream()
                ' Load RSS stream into a DataSet for easier processing
                Dim dsXML As New DataSet()
                dsXML.ReadXml(respStream)
                ' Step 4: Loop through all items returned, displaying results
                Dim target As String = ""
                If NewPage Then
                    target = "target='_new'"
                End If
                For Each item As DataRow In dsXML.Tables("item").Rows
                    ' Step 5: Write the title, link, and description to page
                    writer.WriteLine( _
                       "<a href='" + item("link") + "' " + target + _
                       ">" + item("title") + "</a>" + "<br/>" + _
                       "<span style='color:silver'>" + _
                       item("pubDate") + "</span>" + "<br/>" _
                       )
                    If ShowDescription Then
                        writer.WriteLine("<br/>" + item("description"))
                    End If
                    writer.WriteLine("<hr/>")
                Next
            Catch ex As Exception
                ' Step 3: If error occurs, notify end user
                writer.WriteLine("<font color='red'><strong>" + _
                   "An error occurred while attempting to process " + _
                   "the selected RSS feed. " + _
                   "Please verify that the url provided references " + _
                   "a valid RSS page." _
                   )
            End Try
        End Sub
    End Class
End Namespace

Recipe—C# (See Project RSSWebPartCS, Class RSSWebPart.cs)

using System;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Data;
namespace RSSWebPartCS
{
    public class RSSWebPart : WebPart
    {
        // Local variables to hold web-part
        // property values
        string _url;
        bool _newPage = true;
        bool _showDescription = true;
        bool _showUrl = true;
        // Property to determine whether article should
        // be opened in same or new page
        [Personalizable]
        [WebBrowsable]
        public bool NewPage
        {
            get
            {
                return _newPage;
            }
            set
            {
                _newPage = value;
            }
        }
        // Should description be displayed?
        [Personalizable]
        [WebBrowsable]
        public bool ShowDescription
        {
            get
            {
                return _showDescription;
            }
set
            {
                _showDescription = value;
            }
        }
        // Should URL be displayed?
        [Personalizable]
        [WebBrowsable]
        public bool ShowUrl
        {
            get
            {
                return _showUrl;
            }
            set
            {
                _showUrl = value;
            }
        }
        // Property to set URL of RSS feed
        [Personalizable]
        [WebBrowsable]
        public string Url
        {
            get
            {
                return _url;
            }
            set
            {
                _url = value;
            }
        }
        // This is where the HTML gets rendered to the
        // web-part page.
        protected override void RenderContents(HtmlTextWriter writer)
        {
            base.RenderContents(writer);
            // Step 1: Ensure Url property has been provided
            if (Url != "")
            {
                // Display heading with RSS location URL
                if (ShowUrl)
                {
                    writer.WriteLine("<hr/>");
                    writer.WriteLine("<span style='font-size: larger;'>");
                    writer.WriteLine("Results for: ");
writer.WriteLine("<strong>");
                    writer.WriteLine(Url);
                    writer.WriteLine("</strong>");
                    writer.WriteLine("</span>");
                    writer.WriteLine("<hr/>");
                }
                displayRSSFeed(writer);
            }
            else
            {
                // Tell user they need to fill in the Url property
                writer.WriteLine(
                   "<font color='red'>RSS Url cannot be blank</font>");
            }
        }

        private void displayRSSFeed(HtmlTextWriter writer)
        {
            try
            {
                // Step 2: Read the RSS feed into memory
                System.Net.WebRequest wReq;
                wReq = System.Net.WebRequest.Create(Url);
                wReq.Credentials =
                   System.Net.CredentialCache.DefaultCredentials;
                // Return the response.
                System.Net.WebResponse wResp = wReq.GetResponse();
                System.IO.Stream respStream = wResp.GetResponseStream();
                // Load RSS stream into a DataSet for easier processing
                DataSet dsXML = new DataSet();
                dsXML.ReadXml(respStream);
                // Step 4: Loop through all items returned,
                // displaying results
                string target = "";
                if (NewPage)
                    target = "target='_new'";
                foreach (DataRow item in dsXML.Tables["item"].Rows)
                {
                    // Step 5: Write the title, link, and description to page
                    writer.WriteLine(
                        "<a href='" + item["link"] +
                        "' " + target + ">" + item["title"] + "</a>" +
                        "<br/>" +
                        "<span style='color:silver'>" +
                        item["pubDate"] + "</span>" +
                        "<br/>");
if (ShowDescription)
                        writer.WriteLine("<br/>" + item["description"]);
                    writer.WriteLine("<hr/>");
                }
            }
            catch (Exception ex)
            {
                // Step 3: If error occurs, notify end user
                writer.WriteLine(
                    "<font color='red'><strong>" +
                    "An error occurred while attempting " +
                    "to process the selected RSS feed. " +
                    "Please verify that the url provided " +
                    "references a valid RSS page." +
                    "<BR/><BR/>" +
                    ex.Message;
                );
            }
        }
    }
}

To Run

First, you'll need to install the web part so SharePoint recognizes it. To do so, compile the web part with a strong name. Next, copy the web part .dll to the GAC, which is usually located at C:Windowsassembly. Last, create a <SafeControl> entry in the Web.config file of the target SharePoint web application that looks something like the following:

<SafeControl Assembly="RSSWebPartCS,
   Version=1.0.0.0,
   Culture=neutral,
   PublicKeyToken=5669ee1e85397acc"
   Namespace="RSSWebPartCS"
  TypeName="*"
  Safe="True" />

Of course, the Assembly, Version, PublicKeyToken, and Namespace attribute values will be those you assign rather than those shown in the preceding code.

Note

There are several ways to obtain the information needed to fill out the <SafeControl> element shown in the preceding code, including using the SN.exe (strong name) command or a third-party tool such as Reflector (www.aisto.com/roeder/dotnet/). The simplest in my opinion is to simply copy the signed assembly to the GAC, typically found at C:Windowsassembly. Once there, you can right-click on the assembly name and open the properties dialog box to view the assembly name and public key.

Finally, navigate to the web-part gallery page for the site collection on which you want to place this web part (typically at http://<yourserver>/<sitecol>_catalogs/wp/Forms/AllItems.aspx, where <sitecol> is the root site of the collection). Click the New button and find your web part in the list of web-part type names. Select the checkbox to the left of the web-part type name, and click the Populate Gallery button.

Your web part should now be available to add to any web-part page in the current site collection. Select a web-part page and click the Site Actions

To Run
The custom RSS web-part property pane

Figure 4.1. The custom RSS web-part property pane

Figure 4-2 shows the RSS web part in action.

The custom RSS web part in action

Figure 4.2. The custom RSS web part in action

Variations

  • MOSS does come with an RSS Feed web part, but this recipe adds that functionality to WSS 3.0. To mimic the MOSS web-part functionality, you could allow for the user to define XSLT that determines how the results are rendered.

Creating an XML Web Part

As you saw in the RSS Feed recipe, XML documents can be easily loaded into an ADO.NET DataSet object. And after the XML is in a DataSet, it's a simple matter to format it by using a DataGrid web control, manipulate it programmatically, or transform it by using an XML web control (which, in my opinion would have been better named an XSLT control).

In this recipe, you'll create a generic web part that has several user-configurable properties indicating an XML source URL, whether the XML should be displayed by using a simple DataGrid or transformed by using an XSLT, and optional user credentials to use when accessing secure XML sources.

You might reasonably ask why go to the trouble of creating an XML web part, when there's one that ships with SharePoint or when you could use a DataView web part in SharePoint Designer. The answer is that as a developer, I want understanding and control—understanding of the underlying processes so I can anticipate and avoid problems, and control so I can deliver the specific solution that my end users need. In particular, this recipe addresses how to access secure XML sources by impersonating the currently logged-on user, or any other user, as required. It also enables you to insert any code you need to manipulate the source XML document prior to displaying it to the page.

Note

This recipe builds on many of the concepts introduced in Recipe 4-1, so you may want to review that prior to whipping up an XML web part.

Recipe Type: Web Part

Ingredients

Assembly References

  • System.Web .NET assembly

  • Windows.SharePoint.Services .NET assembly

Special Considerations

  • Because this web part uses the generic ASP.NET 2.0 web-part framework and doesn't need to communicate directly with SharePoint, we won't add a reference to the Windows.SharePoint.Services library. However, if you want to use the legacy SharePoint web-part framework, you will need to add that reference to the project.

Preparation

  1. Create a new C# or VB.NET class library project.

  2. Add a reference to the System.Web .NET assembly.

  3. At the top of the class module, add using or Includes statements for the System.Web.UI.WebControls and System.Web.UI.WebControls.WebParts class libraries.

  4. Open the project properties page, go to the Signing tab, select the Sign the Assembly checkbox, and add a new strong-name key file named something like XMLWebPart.snk.

Process Flow

Process Flow
  1. Make sure that the user has supplied the URL of an XML document to load. If the user has, go to step 2. If not, go to step 3.

  2. If the URL has been provided, check to see whether the user has requested debug information to be displayed, and if so, display that information prior to displaying formatted XML data to the page. Go to step 4.

  3. If the user did not supply a URL to load, display a message indicating that it's required and exit.

  4. Create a new WebRequest object.

  5. If the user has selected the Impersonate web-part property, set the request credentials to be those of the current user.

  6. Otherwise, set the credentials based on the explicitly provided domain, user, and password.

  7. Read the XML document into a DataSet object.

  8. If the user has specified that the data should be displayed by using a DataGrid, go to step 9. Otherwise, go to step 10.

  9. Loop through the collection of tables in the DataSet created in step 7, displaying the table name and a DataGrid containing all data in that table.

  10. Create a new XML web control and set its contents to the XML in memory, read from the DataSet object. Set the XML web control's TransformSource to the path provided in the XSLT property of the web part.

Recipe—VB (See Project XMLWebPartVB, Class XMLWebPart.vb)

Imports System
Imports System.Web
Imports System.Web.Security
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Web.UI.WebControls.WebParts
Imports System.Web.UI.HtmlControls
Imports System.Xml
Imports System.Data
Public Class XMLWebPart
    Inherits WebPart
    ' Local variable to hold property values
    Private _url As String = ""
    Private _impersonate As Boolean = True
    Private _domain As String = ""
    Private _user As String = ""
    Private _password As String = ""
    Private _debug As Boolean = False
    Private _formatUsing As enumFormatUsing = enumFormatUsing.DataGrid
Private _xsltPath As String = ""
    'ENUM types will result in drop-down lists in
    'the web-part property sheet
    Public Enum enumFormatUsing
        DataGrid = 1
        XSLT = 2
    End Enum
    ' Property to set URL of source XML document
    <Personalizable()> _
    <WebBrowsable()> _
    <WebDisplayName("Url of XML document")> _
    Public Property Url() As String
        Get
            Return _url
        End Get
        Set(ByVal value As String)
            _url = value
        End Set
    End Property
    'Create property to determine whether DataGrid or
    'XSLT should be used to format output
    <Personalizable(PersonalizationScope.[Shared]), _
       WebBrowsable(), _
       WebDisplayName("Format Using:"), _
       WebDescription("What method do you want " + _
       "to use to format the results.")> _
    Public Property FormatUsing() As enumFormatUsing
        Get
            Return _formatUsing
        End Get
        Set(ByVal value As enumFormatUsing)
            _formatUsing = value
        End Set
    End Property
    'If XSLT will be used, this property specifies
    'its server-relative path
    <Personalizable(PersonalizationScope.[Shared]), _
      WebBrowsable(), _
      WebDisplayName("XSLT Path:"), _
      WebDescription("If formatting with XSLT, " + _
      "provide full path to XSLT document.")> _
    Public Property XSLTPath() As String
        Get
            Return _xsltPath
        End Get
Set(ByVal value As String)
            _xsltPath = value
        End Set
    End Property
    ' If explicit credentials have been requested,
    ' the following three properties, Domain, User, and
    ' Password, will be used to construct the credentials
    ' to pass to the page
    <Personalizable()> _
    <WebBrowsable()> _
    Public Property Domain() As String
        Get
            Return _domain
        End Get
        Set(ByVal value As String)
            _domain = value
        End Set
    End Property
    <Personalizable()> _
    <WebBrowsable()> _
    Public Property User() As String
        Get
            Return _user
        End Get
        Set(ByVal value As String)
            _user = value
        End Set
    End Property
    <Personalizable()> _
    <WebBrowsable()> _
    Public Property Password() As String
        Get
            Return _password
        End Get
        Set(ByVal value As String)
            _password = value
        End Set
    End Property
    ' If this option is checked, the web part will use
    ' the default credentials of the user viewing
    ' the web-part page.
    <Personalizable()> _
    <WebBrowsable()> _
    Public Property Impersonate() As Boolean
        Get
            Return _impersonate
        End Get
Set(ByVal value As Boolean)
            _impersonate = value
        End Set
    End Property
    ' Display debug info?
    <Personalizable()> _
    <WebBrowsable()> _
    Public Property Debug() As Boolean
        Get
            Return _debug
        End Get
        Set(ByVal value As Boolean)
            _debug = value
        End Set
    End Property
    ' This is where the HTML gets rendered to the
    ' web-part page.
    Protected Overloads Overrides Sub RenderContents( _
       ByVal writer As HtmlTextWriter)
        MyBase.RenderContents(writer)
        ' Step 1: Ensure Url property has been provided
        If Url <> "" Then
            ' Step 2: If debug info requested, display it
            If Debug Then
                writer.WriteLine("Url: " + Url + "<br/>")
                writer.WriteLine("Impersonate: " + _
                   Impersonate.ToString() + "<br/>")
                writer.WriteLine("Domain: " + Domain + "<br/>")
                writer.WriteLine("User: " + User + "<br/>")
                writer.WriteLine("Password: " + Password + "<br/>")
                writer.WriteLine("Format using: " + _
                   FormatUsing.ToString() + "<br/>")
                writer.WriteLine("<hr/>")
            End If
            ' Call helper function to render data as HTML to page
            displayXML(writer)
        Else
            ' Step 3: Tell user they need to fill in the Url property
            writer.WriteLine( _
               "<font color='red'>Source XML url cannot be blank</font>")
        End If
    End Sub
    Private Sub displayXML(ByVal writer As HtmlTextWriter)
        Try
            ' Step 4: Read the XML document into memory
            Dim wReq As System.Net.WebRequest
            wReq = System.Net.WebRequest.Create(Url)
' Step 5: Set the security as appropriate
            If Impersonate Then
                wReq.Credentials = _
                   System.Net.CredentialCache.DefaultCredentials
            Else
                wReq.Credentials = _
                   New System.Net.NetworkCredential(User, Password, Domain)
            End If
            wReq.Credentials = System.Net.CredentialCache.DefaultCredentials
            ' Step 6: Return the response.
            Dim wResp As System.Net.WebResponse = wReq.GetResponse()
            Dim respStream As System.IO.Stream = wResp.GetResponseStream()
            ' Step 7: Load XML stream into a DataSet for easier processing
            Dim dsXML As New DataSet()
            dsXML.ReadXml(respStream)
            ' Step 8: Determine display mechanism to use
            If FormatUsing = enumFormatUsing.DataGrid Then
                ' Step 9: Loop through each table in the DataSet,
                ' displaying each in a DataGrid
                Dim dgXML As DataGrid
                Dim lbl As Label
                For Each dtXML As DataTable In dsXML.Tables
                    ' Display table name
                    lbl = New Label()
                    lbl.Text = "<br/><strong>" + _
                       dtXML.TableName.ToUpper() + "</strong><br/><br/>"
                    lbl.RenderControl(writer)
                    ' Now display the data
                    dgXML = New DataGrid()
                    dgXML.DataSource = dtXML
                    dgXML.DataBind()
                    dgXML.RenderControl(writer)
                Next
            Else
                ' Step 10: Format using provided XSLT
                Dim xml As New System.Web.UI.WebControls.Xml()
                xml.DocumentContent = dsXML.GetXml()
                xml.TransformSource = XSLTPath
                xml.RenderControl(writer)
            End If
        Catch ex As Exception
            ' If error occurs, notify end user
            writer.WriteLine("<font color='red'><strong>" + _
               ex.Message + "</font>")
        End Try
    End Sub
End Class

Recipe—C# (See Project XMLWebPartCS, Class XMLWebPart.cs)

using System;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Data;
namespace XMLWebPartCS
{
    public class XMLWebPart : WebPart
    {
        // Local variable to hold property values
        string _url = "";
        bool _impersonate = true;
        string _domain = "";
        string _user = "";
        string _password = "";
        bool _debug = false;
        enumFormatUsing _formatUsing = enumFormatUsing.DataGrid;
        string _xsltPath = "";
        //ENUM types will result in drop-down lists in
        //the web-part property sheet
        public enum enumFormatUsing
        {
            DataGrid = 1,
            XSLT = 2
        }
        // Property to set URL of source XML document
        [Personalizable]
        [WebBrowsable]
        [WebDisplayName("Url of XML document")]
        public string Url
        {
            get
            {
                return _url;
            }
            set
            {
                _url = value;
            }
        }
//Create property to determine whether DataGrid or
        //XSLT should be used to format output
        [Personalizable(PersonalizationScope.Shared), WebBrowsable(),
            WebDisplayName("Format Using:"),
            WebDescription("What method do you want " +
               "to use to format the results.")]
        public enumFormatUsing FormatUsing
        {
            get { return _formatUsing; }
            set { _formatUsing = value; }
        }
        //If XSLT will be used, this property specifies
        //its server-relative path
        [Personalizable(PersonalizationScope.Shared),
            WebBrowsable(), WebDisplayName("XSLT Path:"),
            WebDescription("If formatting with XSLT, " +
               "provide full path to XSLT document.")]
        public string XSLTPath
        {
            get { return _xsltPath; }
            set { _xsltPath = value; }
        }
        // If explicit credentials have been requested,
        // the following three properties, Domain, User, and
        // Password, will be used to construct the credentials
        // to pass to the page
        [Personalizable]
        [WebBrowsable]
        public string Domain
        {
            get
            {
                return _domain;
            }
            set
            {
                _domain = value;
            }
        }
        [Personalizable]
        [WebBrowsable]
        public string User
        {
            get
            {
                return _user;
            }
set
            {
                _user = value;
            }
        }
        [Personalizable]
        [WebBrowsable]
        public string Password
        {
            get
            {
                return _password;
            }
            set
            {
                _password = value;
            }
        }
        // If this option is checked, the web part will use
        // the default credentials of the user viewing
        // the web-part page.
        [Personalizable]
        [WebBrowsable]
        public bool Impersonate
        {
            get
            {
                return _impersonate;
            }
            set
            {
                _impersonate = value;
            }
        }
        // Display debug info?
        [Personalizable]
        [WebBrowsable]
        public bool Debug
        {
            get
            {
                return _debug;
            }
            set
            {
                _debug = value;
            }
        }
// This is where the HTML gets rendered to the
        // web-part page.
        protected override void RenderContents(HtmlTextWriter writer)
        {
            base.RenderContents(writer);
            // Step 1: Ensure Url property has been provided
            if (Url != "")
            {
                // Step 2: If debug info requested, display it
                if (Debug)
                {
                    writer.WriteLine("Url: " + Url + "<br/>");
                    writer.WriteLine("Impersonate: " +
                       Impersonate.ToString() + "<br/>");
                    writer.WriteLine("Domain: " + Domain + "<br/>");
                    writer.WriteLine("User: " + User + "<br/>");
                    writer.WriteLine("Password: " + Password + "<br/>");
                    writer.WriteLine("Format using: " + FormatUsing +
                       "<br/>");
                    writer.WriteLine("<hr/>");
                }
                // Call helper function to render data as HTML to page
                displayXML(writer);
            }
            else
            {
                // Step 3: Tell user they need to fill in the Url property
                writer.WriteLine(
                   "<font color='red'>Source XML url cannot be blank</font>");
            }
        }
        private void displayXML(HtmlTextWriter writer)
        {
            try
            {
                // Step 4: Read the XML data into memory
                System.Net.WebRequest wReq;
                wReq = System.Net.WebRequest.Create(Url);
                // Step 5: Set the security as appropriate
                if (Impersonate)
                {
                    wReq.Credentials =
                       System.Net.CredentialCache.DefaultCredentials;
                }
else
                {
                    wReq.Credentials = new System.Net.NetworkCredential(
                       User, Password, Domain);
                }
                wReq.Credentials =
                   System.Net.CredentialCache.DefaultCredentials;
                // Step 6: Return the response.
                System.Net.WebResponse wResp = wReq.GetResponse();
                System.IO.Stream respStream = wResp.GetResponseStream();
                // Step 7: Load XML stream into a DataSet for easier
                // processing
                DataSet dsXML = new DataSet();
                dsXML.ReadXml(respStream);
                // Step 8: Determine display mechanism to use
                if (FormatUsing == enumFormatUsing.DataGrid)
                {
                    // Step 9: Loop through each table in the DataSet,
                    // displaying each in a DataGrid
                    DataGrid dgXML;
                    Label lbl;
                    foreach (DataTable dtXML in dsXML.Tables)
                    {
                        // Display table name
                        lbl = new Label();
                        lbl.Text = "<br/><strong>" +
                           dtXML.TableName.ToUpper() +
                           "</strong><br/><br/>";
                        lbl.RenderControl(writer);
                        // Now display the data
                        dgXML = new DataGrid();
                        dgXML.DataSource = dtXML;
                        dgXML.DataBind();
                        dgXML.RenderControl(writer);
                    }
                }
                else
                {
                    // Step 10: Format using provided XSLT
                    System.Web.UI.WebControls.Xml xml =
                       new System.Web.UI.WebControls.Xml();
                    xml.DocumentContent = dsXML.GetXml();
                    xml.TransformSource = XSLTPath;
                    xml.RenderControl(writer);
                }
            }
catch (Exception ex)
            {
                // If error occurs, notify end user
                writer.WriteLine("<font color='red'><strong>" +
                   ex.Message + "</font>");
            }
        }
    }
}

Recipe—XML Document (See Project XMLWebPartCS, File SampleXMLSource.xml)

The following listing provides the sample XML document that was used in conjunction with the XSLT that appears in the next section. The XML web part is designed, however, to work with any valid XML document.

<?xml version="1.0" encoding="utf-8"?>
<CustomerData>
     <Customer>
          <Name>ABC Corp.</Name>
          <Address>100 Main Street</Address>
          <Phone>(415) 999-1234</Phone>
          <Order>
               <OrderNo>1000</OrderNo>
               <Product>Widgets</Product>
               <Qty>10</Qty>
               <UnitPrice>100</UnitPrice>
               <ExtPrice>1000</ExtPrice>
          </Order>
          <Order>
               <OrderNo>1001</OrderNo>
               <Product>Gadget</Product>
               <Qty>50</Qty>
               <UnitPrice>50</UnitPrice>
               <ExtPrice>2500</ExtPrice>
          </Order>
          <Order>
               <OrderNo>0113</OrderNo>
               <Product>WhatsIt</Product>
               <Qty>100</Qty>
               <UnitPrice>70</UnitPrice>
               <ExtPrice>7000</ExtPrice>
          </Order>
     </Customer>
<Customer>
          <Name>XYZ Inc.</Name>
          <Address>123 Center Avenue</Address>
          <Phone>(650) 789-1234</Phone>
          <Order>
               <OrderNo>2000</OrderNo>
               <Product>Laptop</Product>
               <Qty>10</Qty>
               <UnitPrice>1000</UnitPrice>
               <ExtPrice>10000</ExtPrice>
          </Order>
          <Order>
               <OrderNo>2001</OrderNo>
               <Product>Memory</Product>
               <Qty>50</Qty>
               <UnitPrice>100</UnitPrice>
               <ExtPrice>5000</ExtPrice>
          </Order>
          <Order>
               <OrderNo>2003</OrderNo>
               <Product>LCD</Product>
               <Qty>100</Qty>
               <UnitPrice>300</UnitPrice>
               <ExtPrice>30000</ExtPrice>
          </Order>
     </Customer>
</CustomerData>

Recipe—XSLT

You could, of course, create any number of XSLT transforms to format the sample data. That's exactly the point of XSLT: to allow you to separate the source data from the means to display it. The following XSLT was used to provide the sample output in the "To Run" section.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="html"/>
   <xsl:template match="/">
      <xsl:for-each select="CustomerData/Customer">
         <h1>Orders for <xsl:value-of select="Name"/></h1>
         Address: <xsl:value-of select="Address"/><br/>
         Phone: <xsl:value-of select="Phone"/><br/><br/>
         <table cellpadding="3" cellspacing="3" width="80%">
            <tr valign="bottom">
               <td>
                  <strong>
                     <u>Order #</u>
                  </strong>
               </td>
<td>
                  <strong>
                     <u>Product</u>
                  </strong>
               </td>
               <td align="center">
                  <strong>
                     <u>Quanty</u>
                  </strong>
               </td>
               <td align="right">
                  <strong>
                     Unit<br/><u>Price</u>
                  </strong>
               </td>
               <td align="right">
                  <strong>
                     Extended<br/><u>Price</u>
                  </strong>
               </td>
            </tr>
            <xsl:for-each select="Order">
               <tr>
                  <td>
                     <xsl:value-of select="OrderNo"/>
                  </td>
                  <td>
                     <xsl:value-of select="Product"/>
                  </td>
                  <td align="center">
                     <xsl:value-of select="Qty"/>
                  </td>
                  <td align="right">
                  <xsl:value-of select="format-number(UnitPrice,'$ #,###')"/>
                  </td>
                  <td align="right">
                  <xsl:value-of select="format-number(ExtPrice,'$ #,###')"/>
                  </td>
               </tr>
            </xsl:for-each>
<tr>
               <td colspan="4"/>
               <td align="right">
                  ==========
               </td>
            </tr>
            <tr>
               <td colspan="4"/>
               <td align="right">
        <xsl:value-of select="format-number(sum(Order/ExtPrice),'$ #,###')"/>
               </td>
            </tr>
         </table>
      </xsl:for-each>
   </xsl:template>
</xsl:stylesheet>

To Run

Note

Please see the "To Run" section of Recipe 4-1 for instructions on how to deploy a web part to a single site collection. After the web part has been successfully deployed, proceed with the following steps.

After the web part has been successfully deployed, you can add your custom XML web part to any page in the site collection, open the web-part property sheet, and provide a URL to an XML document.

Note

The SampleSourceXML used for the following example should be saved to a folder that is served by IIS or some other web server, and that can be read from your SharePoint server.

After you have set the URL property of the web part and selected the Debug checkbox, save your changes to display a result similar to that shown in Figure 4-3.

The XML web part in debug mode

Figure 4.3. The XML web part in debug mode

Note that ADO.NET has inserted a Customer_Id column that did not appear in the source XML. .NET does this to maintain the parent-child relationship that is implicit in the XML.

Now let's spruce things up a bit. Open the web-part property pane and select XSLT from the Format drop-down list. Next enter a server-relative path to an XSLT document.

Note

The XSLT can simply be copied to a shared location on your SharePoint server. Then enter the Universal Naming Convention (UNC) of that location.

The resulting web-part output will look something like that shown in Figure 4-4.

Fully formatted output using the XML web part

Figure 4.4. Fully formatted output using the XML web part

Variations

  • Modify the recipe to allow the XSLT to be retrieved from a document library in SharePoint rather than from the physical server, to allow for collaboration and development directly within the site.

Creating a SQL Web Part

This recipe shows you how to create one of the most useful web parts, one that can be used to query and format any SQL data source that can be accessed from your SharePoint server. At its core, this web part is quite simple. It does two things: 1) queries a SQL data source and places the results of the query into a DataSet in memory, and 2) uses XSLT to format the result set and display it on the page using HTML. In those two simple steps, you'll find a vast number of solutions to the problem of formatting external SQL data sources.

Recipe Type: Web Part

Ingredients

Class Library References

  • System.Web.UI.WebControls.WebParts class library

  • System.Web.UI.WebControls class library

Special Considerations

  • Remember that the queries are being executed from the SharePoint web server, so that server must have the ability to communicate with the target SQL server.

  • Because this web part uses the generic ASP.NET 2.0 web-part framework and doesn't need to communicate directly with SharePoint, we won't add a reference to the Windows.SharePoint.Services library. However, if you want to use the legacy SharePoint web-part framework, you will need to add that reference to the project.

Preparation

  1. Create a new C# or VB.NET class library project.

  2. Add a reference to the System.Web .NET assembly.

  3. At the top of the class module, add using or Includes statements for the System.Web.UI.WebControls and System.Web.UI.WebControls.WebParts class libraries.

  4. Open the project properties page, go to the Signing tab, select the Sign the Assembly checkbox, and add a new strong-name key file named something like SQLWebPart.snk.

Process Flow

Process Flow
  1. If the web part's debug property has been selected, display the property settings prior to displaying the formatted output.

  2. Query the database to return one or more tables in a result set.

  3. Load the result set into a DataSet object.

  4. Determine whether the format property has been set to DataGrid or XSLT.

  5. If it has been set to DataGrid, assign the DataSet as the source for a DataGrid web control and display that.

  6. Otherwise, assign the specified XSLT document to the TransformSource property of the XML web control, assign the DataSet as the document, and then add the control to the page.

Recipe—VB (See Project SQLWebPartVB, Class SQLWebPart.vb)

Imports System.Web.UI.WebControls
Imports System.Web.UI.WebControls.WebParts
Imports System.Data
Imports System.Xml
Public Class SQLWebPart
    Inherits WebPart
    'Define local variables to contain property values
    Private _connectionString As String = ""
    Private _connectionKey As String = ""
    Private _query As String = ""
    Private _formatUsing As enumFormatUsing = enumFormatUsing.DataGrid
    Private _xsltPath As String = ""
    Private _includeDebugInfo As Boolean = False
    'ENUM types will result in drop-down lists in
    'the web-part property sheet
    Public Enum enumFormatUsing
        DataGrid = 1
        XSLT = 2
    End Enum
    'Create property to hold SQL connection string
    <Personalizable( _
        PersonalizationScope.Shared), _
        WebBrowsable(), _
        WebDisplayName("Connection String:"), _
        WebDescription("Connection string to use" & _
           " when connecting to SQL source.")> _
    Property ConnectionString() As String
        Get
            Return _connectionString
        End Get
Set(ByVal Value As String)
            _connectionString = Value
        End Set
    End Property
    'Create property to hold SQL query
    <Personalizable( _
        PersonalizationScope.Shared), _
        WebBrowsable(), _
        WebDisplayName("SQL Query:"), _
        WebDescription("A valid SQL query to execute.")> _
    Property Query() As String
        Get
            Return _query
        End Get
        Set(ByVal Value As String)
            _query = Value
        End Set
    End Property
    'Create property to determine whether DataGrid or
    'XSLT should be used to format output
    <Personalizable( _
        PersonalizationScope.Shared), _
        WebBrowsable(), WebDisplayName("Format Using:"), _
        WebDescription("What method do you want " & _
            "to use to format the results.")> _
    Property FormatUsing() As enumFormatUsing
        Get
            Return _formatUsing
        End Get
        Set(ByVal Value As enumFormatUsing)
            _formatUsing = Value
        End Set
    End Property
    'If XSLT will be used, this property specifies
    'its path
    <Personalizable( _
        PersonalizationScope.Shared), _
        WebBrowsable(), _
        WebDisplayName("XSLT Path:"), _
        WebDescription("If formatting with XSLT, " & _
            "provide full path to XSLT document.")> _
    Property XSLTPath() As String
        Get
            Return _xsltPath
        End Get
Set(ByVal Value As String)
            _xsltPath = Value
        End Set
    End Property
    'Even though our web parts never have bugs...
    <Personalizable( _
        PersonalizationScope.Shared), _
        WebBrowsable(), _
        WebDisplayName("Include Debug Info?:"), _
        WebDescription("If selected, will " & _
            "display values of web part properties.")> _
    Property IncludeDebugInfo() As Boolean
        Get
            Return _includeDebugInfo
        End Get
        Set(ByVal Value As Boolean)
            _includeDebugInfo = Value
        End Set
    End Property
    'This is where the real work happens!
    Protected Overrides Sub RenderContents( _
          ByVal writer As System.Web.UI.HtmlTextWriter)
        'Process any output from the base class first
        MyBase.RenderContents(writer)
        ' Step 1: Display debug info if requested
        If IncludeDebugInfo Then
            writer.Write("Connection String: " & ConnectionString)
            writer.WriteBreak()
            writer.Write("SQL Query: " & Query)
            writer.WriteBreak()
            writer.Write("Format Using: " & FormatUsing.ToString)
            writer.WriteBreak()
            writer.Write("XSLT Path: " & XSLTPath)
            writer.Write("<hr>")
        End If
        ' Step 2: Query SQL database and return the result set
        Dim con As New SqlClient.SqlConnection(ConnectionString)
        Try
            con.Open()
        Catch ex As Exception
            writer.Write("<font color='red'>" & ex.Message & "</font>")
            Exit Sub
        End Try
        Dim da As New SqlClient.SqlDataAdapter(Query, con)
        Dim ds As New DataSet
        ' Step 3: Copy result set to DataSet
Try
            da.Fill(ds)
        Catch ex As Exception
            writer.Write("<font color='red'>" & ex.Message & "</font>")
            Exit Sub
        End Try
        ' Step 4: Format the output using an XSLT or DataGrid
        If FormatUsing = enumFormatUsing.DataGrid Then
            ' Step 5: Format using simple DataGrid
            Dim dg As New DataGrid
            dg.DataSource = ds
            dg.DataBind()
            dg.RenderControl(writer)
        Else
            ' Step 6: Format using provided XSLT
            Dim xml As New System.Web.UI.WebControls.Xml
            xml.DocumentContent = ds.GetXml
            xml.TransformSource = XSLTPath
            xml.RenderControl(writer)
        End If
    End Sub
End Class

Recipe—C# (See Project SQLWebPartCS, Class SQLWebPartCS.cs)

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Data;
using System.Xml;
namespace SQLWebPartCS
{
    public class SQLWebPartCS : WebPart
    {
        //Define local variables to contain property values
        string _connectionString = "";
        string _query = "";
        enumFormatUsing _formatUsing = enumFormatUsing.DataGrid;
        string _xsltPath = "";
        bool _includeDebugInfo = false;
        //ENUM types will result in drop-down lists in
        //the web-part property sheet
public enum enumFormatUsing
        {
            DataGrid = 1,
            XSLT = 2
        }
        //Create property to hold SQL connection string
        [Personalizable(PersonalizationScope.Shared),
           WebBrowsable(), WebDisplayName("Connection String:"),
           WebDescription("Connection string to use" +
           " when connecting to SQL source.")]
        public string ConnectionString
        {
            get { return _connectionString; }
            set { _connectionString = value; }
        }
        //Create property to hold SQL query
        [Personalizable(PersonalizationScope.Shared),
           WebBrowsable(), WebDisplayName("SQL Query:"),
          WebDescription("A valid SQL query to execute.")]
        public string Query
        {
            get { return _query; }
            set { _query = value; }
        }
        //Create property to determine whether DataGrid or
        //XSLT should be used to format output
        [Personalizable(PersonalizationScope.Shared),
           WebBrowsable(), WebDisplayName("Format Using:"),
           WebDescription("What method do you want " +
           "to use to format the results.")]
        public enumFormatUsing FormatUsing
        {
            get { return _formatUsing; }
            set { _formatUsing = value; }
        }
        //If XSLT will be used, this property specifies
        //its path
        [Personalizable(PersonalizationScope.Shared),
           WebBrowsable(), WebDisplayName("XSLT Path:"),
           WebDescription("If formatting with XSLT, " +
           "provide full path to XSLT document.")]
        public string XSLTPath
        {
            get { return _xsltPath; }
            set { _xsltPath = value; }
        }
//Even though our web parts never have bugs...
        [Personalizable(PersonalizationScope.Shared),
           WebBrowsable(), WebDisplayName("Include Debug Info?:"),
           WebDescription("If selected, will " +
           "display values of web part properties.")]
        public bool IncludeDebugInfo
        {
            get { return _includeDebugInfo; }
            set { _includeDebugInfo = value; }
        }
        //This is where the real work happens!
        protected override void RenderContents(
           System.Web.UI.HtmlTextWriter writer)
        {
            //Process any output from the base class first
            base.RenderContents(writer);
            // Step 1: Display debug info if requested
            if (IncludeDebugInfo)
            {
                writer.Write("Connection String: " + ConnectionString);
                writer.WriteBreak();
                writer.Write("SQL Query: " + Query);
                writer.WriteBreak();
                writer.Write("Format Using: " + FormatUsing.ToString());
                writer.WriteBreak();
                writer.Write("XSLT Path: " + XSLTPath);
                writer.Write("<hr>");
            }
            // Step 2: Query SQL database and return the result set
            System.Data.SqlClient.SqlConnection con =
               new System.Data.SqlClient.SqlConnection(ConnectionString);
            try
            {
                con.Open();
            }
            catch (Exception ex)
            {
                writer.Write("<font color='red'>" + ex.Message + "</font>");
                return;
            }
            System.Data.SqlClient.SqlDataAdapter da =
               new System.Data.SqlClient.SqlDataAdapter(Query, con);
            DataSet ds = new DataSet();
// Step 3: Copy result set to DataSet
            try
            {
                da.Fill(ds);
            }
            catch (Exception ex)
            {
                writer.Write("<font color='red'>" + ex.Message + "</font>");
                return;
            }
            // Step 4: Format the output using an XSLT or DataGrid
            if (FormatUsing == enumFormatUsing.DataGrid)
            {
                // Step 5: Format using simple DataGrid
                DataGrid dg = new DataGr    id();
                dg.DataSource = ds;
                dg.DataBind();
                dg.RenderControl(writer);
            }
            else
            {
                // Step 6: Format using provided XSLT
                System.Web.UI.WebControls.Xml xml =
                   new System.Web.UI.WebControls.Xml();
                xml.DocumentContent = ds.GetXml();
                xml.TransformSource = XSLTPath;
                xml.RenderControl(writer);
            }
        }
    }
}

Recipe—XSLT (See Project SQLWebPartVB, File Presidents.xslt)

As I've noted before, XML Transformations (XSLT) is an incredibly powerful technology for manipulating any XML source, including of course the contents of any .NET DataSet or DataTable object. XSLT can be used to render XML as HTML for display, or to write XML to a new XML document with a different structure. In this case, we want to use XSLT to render our sample data as HTML for display on a web-part page. The following XSLT will be used in our example.

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
     <table cellpadding="3" cellspacing="0">
          <tr>
               <td>
                    <u><strong>President</strong></u>
               </td>
<td align="center">
                    <u>
                         <strong>Years In Office</strong>
                    </u>
               </td>
               <td style="width: 10px"/>
               <td style="background-color: silver; width: 1px"/>
               <td style="width: 10px"/>
               <td>
                    <u><strong>President</strong></u>
               </td>
               <td>
                    <u><strong>Years In Office</strong></u>
               </td>
          </tr>
          <xsl:for-each select="NewDataSet/Table">
               <xsl:if test="position() mod 2 = 1">
                    <xsl:text disable-output-escaping="yes">
                       &lt;tr&gt;
                    </xsl:text>
               </xsl:if>
               <td>
                    <xsl:value-of select="Name"/>
               </td>
               <td align="center">
                    <xsl:value-of select="YearsInOffice"/>
               </td>
               <xsl:if test="position() mod 2 = 1">
                    <td style="width: 10px"/>
                    <td style="background-color: silver; width: 1px"/>
                    <td style="width: 10px"/>
               </xsl:if>
               <xsl:if test="position() mod 2 = 0">
                    <xsl:text disable-output-escaping="yes">
                         &lt;/tr&gt;
                    </xsl:text>
               </xsl:if>
          </xsl:for-each>
     </table>
</xsl:template>
</xsl:stylesheet>

Note

Although Visual Studio .NET has very basic XML and XSLT editing capabilities, you may want to investigate third-party editors such as Stylus Studio from Progress Software, or XMLSpy from Altova.

To Run

After you have added the SQL web part to a web-part page, you're ready to try it out.

Note

Please see the "To Run" section of Recipe 4-1 for instructions on how to deploy a web part to a single site collection. After the web part has been successfully deployed, proceed with the following steps.

The following example assumes the SQL data source provided in Table 4-1.

Table 4.1. Presidents SQL Table Definition

Database name

MyDatabase

Table name

Presidents

User login

MyDatabaseUser, password password

Connection string

Data Source=MGEROW-MOSS-VPC; Initial Catalog=MyDatabase; UID=MyDatabaseUser; PWD=password

SQL query

select * from Presidents

Table definition

CREATE TABLE [dbo].[Presidents]( [Name] [varchar](50) NULL [YearsInOffice] [varchar](50) NULL, [Id] [int] IDENTITY(1,1) NOT NULL)

Of course, you will likely use a different server, and there is no requirement that you even use the same query—although if you choose to change the query, you will also need to change the XSLT accordingly.

Figure 4-5 shows the SQL web part in action. Figure 4-6 shows the associated custom properties on the web-part property sheet.

The Presidents table displayed by using the SQL web part

Figure 4.5. The Presidents table displayed by using the SQL web part

The SQL web part's property pane

Figure 4.6. The SQL web part's property pane

Variations

  • One of the more interesting variations on the preceding example is to process more than one SQL result set at a time. This can be accomplished by specifying a Microsoft SQL stored procedure that returns multiple tables, rather than a simple SQL query, in the SQL Query parameter. The XSLT is then written to process multiple tables rather than just one. The final formatting can be quite involved, presenting exciting possibilities for rapid solutions when presenting complex business data.

Creating a Page Viewer Web Part

As with the XML web part, you might be wondering, "Why create a Page Viewer web part when one ships with SharePoint?" The answer is, to gain more flexibility and control. For example, what do you do if the page you want to access requires authentication? The built-in Page Viewer web part doesn't provide any way to pass credentials to the page to be displayed.

Further, what if you want to perform some transformation on the page you're acquiring before displaying it? For example, suppose you want only a fragment of the page (what used to be referred to as screen scraping)? With our custom page viewer, we could add code to parse the HTML returned by the web page, extract the desired content, and display only that.

Recipe Type: Web Part

Ingredients

Class Library References

  • System.Web.UI.WebControls.WebParts class library

  • System.Web.UI.WebControls class library

Special Considerations

  • Because this web part uses the generic ASP.NET 2.0 web-part framework and doesn't need to communicate directly with SharePoint, we won't add a reference to the Windows.SharePoint.Services library. However, if you want to use the legacy SharePoint web-part framework, you will need to add that reference to the project.

  • This web part supports one of two authentication modes: 1) impersonation, where the web part will pass the currently logged-in user's credentials to the target page, or 2) explicit, where a user domain, name, and password are entered directly into the web-part's property sheet.

  • Because this recipe loads the source HTML into the current SharePoint page, relative links on the source page to resources such as images, Cascading Style Sheets (CSS) sheets, JavaScript files, or hyperlinks will not work. So you will either need to find all <A> tags and fix the relative links, or use the alternative approach of creating an <IFRAME> (discussed at the end of this recipe).

Warning

Manipulating a web-part page's HTML by using JavaScript and the Document Object Model (DOM) may cause problems on the SharePoint page if there is embedded JavaScript in the target HTML that conflicts with, or overrides, the native SharePoint script. Because of these issues, you should use this technique only when you have a thorough understanding of or control over the target web page.

Preparation

  1. Create a new C# or VB.NET class library project.

  2. Add a reference to the System.Web .NET assembly.

  3. At the top of the class module, add using or Includes statements for the System.Web.UI.WebControls and System.Web.UI.WebControls.WebParts class libraries.

  4. Open the project properties page, go to the Signing tab, select the Sign the Assembly checkbox, and add a new strong-name key file named something like SQLWebPart.snk.

Process Flow

Process Flow
  1. If the Debug property has been selected on the web-part properties page, write all properties to the web-part page.

  2. If the URL property was not provided, there's nothing more to do. Warn the user that a URL is required and stop processing.

  3. Create a .NET web request object to read the specified URL into memory.

  4. Either assign the current user credentials (if Impersonate is selected), or the explicit domain/user/password provided to the web request before requesting the page.

  5. Attempt to read the specified URL with the given credentials. If an error occurs, display it to the web-part page. If no error occurs, go to step 6.

  6. Display the contents of the URL read.

Recipe—VB (See Project PageViewerWebPartVB, Class PageViewerWebPartVB.vb)

Imports System
Imports System.Web
Imports System.Web.Security
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Web.UI.WebControls.WebParts
Imports System.Web.UI.HtmlControls
Imports System.Xml
Imports System.Data
Public Class PageViewerWebPartVB
    Inherits WebPart
    ' Local variable to hold property values
    Private _url As String = ""
    Private _impersonate As Boolean = True
    Private _domain As String = ""
    Private _user As String = ""
    Private _password As String = ""
    Private _debug As Boolean = False
    ' Property to set URL of page to display
    <Personalizable(), _
     WebBrowsable()> _
    Public Property Url() As String
        Get
            Return _url
        End Get
        Set(ByVal value As String)
            _url = value
        End Set
    End Property
' If explicit credentials have been requested,
    ' the following three properties will
    ' be used to construct the credentials
    ' to pass to the page
    <Personalizable(), _
     WebBrowsable()> _
    Public Property Domain() As String
        Get
            Return _domain
        End Get
        Set(ByVal value As String)
            _domain = value
        End Set
    End Property
    <Personalizable(), _
     WebBrowsable()> _
    Public Property User() As String
        Get
            Return _user
        End Get
        Set(ByVal value As String)
            _user = value
        End Set
    End Property
    <Personalizable(), _
     WebBrowsable()> _
    Public Property Password() As String
        Get
            Return _password
        End Get
        Set(ByVal value As String)
            _password = value
        End Set
    End Property
    ' Should user be impersonated?
    <Personalizable(), _
     WebBrowsable()> _
    Public Property Impersonate() As Boolean
        Get
            Return _impersonate
        End Get
        Set(ByVal value As Boolean)
            _impersonate = value
        End Set
    End Property
' Display debug info?
    <Personalizable(), _
     WebBrowsable()> _
    Public Property Debug() As Boolean
        Get
            Return _debug
        End Get
        Set(ByVal value As Boolean)
            _debug = value
        End Set
    End Property
    ' This is where the HTML gets rendered to the
    ' web-part page.
    Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
        MyBase.RenderContents(writer)
        ' Step 1: If debug info requested, display it
        If Debug Then
            writer.WriteLine("Url: " + Url + "<br/>")
            writer.WriteLine("Impersonate: " + Impersonate.ToString _
               + "<br/>")
            writer.WriteLine("Domain: " + Domain + "<br/>")
            writer.WriteLine("User: " + User + "<br/>")
            writer.WriteLine("Password: " + Password + "<br/>")
            writer.WriteLine("<hr/>")
        End If
        ' Step 2: Make sure URL is provided
        If (Url = "") Then
            writer.WriteLine( _
               "<font color='red'>Please enter a valid Url</font>")
            Return
        End If
        ' Step 3: Create a web request to read desired page
        Dim wReq As System.Net.WebRequest
        wReq = System.Net.WebRequest.Create(Url)
        ' Step 4: Set the security as appropriate
        If Impersonate Then
            wReq.Credentials = System.Net.CredentialCache.DefaultCredentials
        Else
            wReq.Credentials = _
               New System.Net.NetworkCredential(User, Password, Domain)
        End If
        ' Step 5: Get the page contents as a string variable
Try
            Dim wResp As System.Net.WebResponse = wReq.GetResponse
            Dim respStream As System.IO.Stream = wResp.GetResponseStream
            Dim respStreamReader As System.IO.StreamReader = _
               New System.IO.StreamReader(respStream, _
               System.Text.Encoding.ASCII)
            Dim strHTML As String = respStreamReader.ReadToEnd
            ' Step 6: Render the HTML to the web-part page
            writer.Write(strHTML)
        Catch e As Exception
            writer.Write(("<font color='red'>" + e.Message))
        End Try
    End Sub
End Class

Recipe—C# (See Project PageViewerWebPartCS, Class PageViewerWebPartCS.cs)

using System;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Data;
namespace PageViewerWebPartCS
{
    public class PageViewerWebPartCS : WebPart
    {
        // Local variable to hold property values
        string _url = "";
        bool _impersonate = true;
        string _domain = "";
        string _user = "";
        string _password = "";
        bool _debug = false;
        // Property to set URL of page to display
        [Personalizable]
        [WebBrowsable]
        public string Url
        {
            get
            {
                return _url;
            }
set
            {
                _url = value;
            }
        }
        // If explicit credentials have been requested,
        // the following three properties will
        // be used to construct the credentials
        // to pass to the page
        [Personalizable]
        [WebBrowsable]
        public string Domain
        {
            get
            {
                return _domain;
            }
            set
            {
                _domain = value;
            }
        }
        [Personalizable]
        [WebBrowsable]
        public string User
        {
            get
            {
                return _user;
            }
            set
            {
                _user = value;
            }
        }
        [Personalizable]
        [WebBrowsable]
        public string Password
        {
            get
            {
                return _password;
            }
            set
            {
                _password = value;
            }
        }
// Should user be impersonated?
        [Personalizable]
        [WebBrowsable]
        public bool Impersonate
        {
            get
            {
                return _impersonate;
            }
            set
            {
                _impersonate = value;
            }
        }
        // Display debug info?
        [Personalizable]
        [WebBrowsable]
        public bool Debug
        {
            get
            {
                return _debug;
            }
            set
            {
                _debug = value;
            }
        }
        // This is where the HTML gets rendered to the
        // web-part page.
        protected override void RenderContents(HtmlTextWriter writer)
        {
            base.RenderContents(writer);
            // Step 1: If debug info requested, display it
            if (Debug)
            {
                writer.WriteLine("Url: " + Url + "<br/>");
                writer.WriteLine("Impersonate: " + Impersonate.ToString() +
                   "<br/>");
                writer.WriteLine("Domain: " + Domain + "<br/>");
                writer.WriteLine("User: " + User + "<br/>");
                writer.WriteLine("Password: " + Password + "<br/>");
                writer.WriteLine("<hr/>");
            }
            // Step 2: Make sure URL is provided
if (Url == "")
            {
                writer.WriteLine(
                   "<font color='red'>Please enter a valid Url</font>");
                return;
            }
            // Step 3: Create a web request to read desired page
            System.Net.WebRequest wReq;
            wReq = System.Net.WebRequest.Create(Url);
            // Step 4: Set the security as appropriate
            if (Impersonate)
            {
                wReq.Credentials =
                   System.Net.CredentialCache.DefaultCredentials;
            }
            else
            {
                wReq.Credentials =
                   new System.Net.NetworkCredential(User, Password, Domain);
            }
            // Step 5: Get the page contents as a string variable
            try
            {
                System.Net.WebResponse wResp = wReq.GetResponse();
                System.IO.Stream respStream = wResp.GetResponseStream();
                System.IO.StreamReader respStreamReader =
                   new System.IO.StreamReader(respStream,
                   System.Text.Encoding.ASCII);
                string strHTML = respStreamReader.ReadToEnd();
                // Step 6: Render the HTML to the web-part page
                writer.Write(strHTML);
            }
            catch (Exception e)
            {
                writer.Write("<font color='red'>" + e.Message);
            }
        }
    }
}

To Run

Note

Please see the "To Run" section of Recipe 4-1 for instructions on how to deploy a web part to a single site collection. After the web part has been successfully deployed, proceed with the following steps.

After you have deployed the web part to your site collection, place an instance of the web part on a web-part page. Initially an error message will be displayed stating that you must provide a URL to a web page. To do so, open the web-part property sheet, fill in the URL, and click the OK button. The result is shown in Figure 4-7.

A simple web page displayed by using the custom Page Viewer web part

Figure 4.7. A simple web page displayed by using the custom Page Viewer web part

Assuming that the account under which you are currently logged in has permissions to that URL (or the site allows anonymous access), the page should be displayed. If your current account doesn't have the necessary permissions, you can deselect the Impersonate property and provide a specific domain, user, and password. Figure 4-8 shows the property settings to force the custom Page Viewer web part to connect as the user WebPartUser.

The custom Page Viewer web part's property pane

Figure 4.8. The custom Page Viewer web part's property pane

Variations

  • As noted earlier, this web part provides significant flexibility not available in the out-of-the-box Page Viewer web part provided with SharePoint. One variation is to create a custom version of this web part to extract a known portion of another page. This might be appropriate if you have an internal web site that provides some useful data on a page, but all you want is a part of that page. Assuming you know the structure of the underlying HTML of that page, you could extract the desired HTML fragment by using string manipulation after the page has been read into a .NET string variable, writing just that fragment out to the web-part page.

  • Rather than allow the end user to explicitly enter a domain, user name, and password, another variation is to store that information in an external source such as SQL Server or SharePoint's Web.config file. The credential information could be looked up based on the value of the URL.

  • Make the URL property a drop-down list rather than a text box to allow end users to select from a controlled list of pages to display.

  • As noted in the "Special Considerations" section at the beginning of this recipe, the preceding approach has the disadvantage that relative links on the source page will be broken. An alternative to reading the page into a string variable is to construct an <IFRAME> tag to hold the source. The disadvantage is that you cannot then handle authentication or perform any processing on the page before you display it. To use the <IFRAME> approach, replace the code in the RenderContents() method with a single statement that looks something like the following:

    writer.Write("<IFRAME src=" + Url + " FRAMEBORDER='none'
         ALIGN='TOP' VSPACE='0' scrolling='no' WIDTH='100%' HEIGHT='100%'>
    Variations
    </IFRAME>");

Creating a Connectable Page Viewer Web Part

One of the most exciting aspects of web-part technology is the ability to pass data between web parts. This capability enables web parts to interoperate in complex and flexible ways, and enables well-designed web parts to be used in ways that weren't originally anticipated at design time.

This recipe is for one such web part, which is a variation of the earlier custom Page Viewer web part that enables your users to select predefined target sites from a drop-down list. The drop-down list is populated from a list of sites that you can define via a delimited list.

Recipe Type: Web Part

Ingredients

Class Library References

  • System.Web.UI.WebControls.WebParts class library

  • System.Web.UI.WebControls class library

Special Considerations

  • Because this web part uses the generic ASP.NET 2.0 web-part framework and doesn't need to communicate directly with SharePoint, we won't add a reference to the Windows.SharePoint.Services library. However, if you want to use the legacy SharePoint web-part framework, you will need to add that reference to the project.

  • In this example, I have placed the custom interface and both web-part classes in the sample project class file. In most instances you will probably place the interface in its own file or even its own project so that it may be easily shared between any number of web parts that need to use that interface.

  • This recipe shows an alternative approach to displaying a web page's contents to that shown in Recipe 4-4. In that case, we used the ASP.NET System.Net.WebRequest class to read the contents of the target page into memory and then write it out to our web-part page. In this recipe, we simply render an <IFRAME> tag to the web-part page with the SRC attribute set to the target URL. The first approach has the advantage that we can explicitly pass credentials, which may or may not be those of the currently logged-in user, to the target page. We can also manipulate the returned HTML prior to rendering if we wish because we have a copy of that HTML in memory. One disadvantage of the WebRequest approach is that relative references (such as relative HREF attributes in <A> tags, or SRC attributes referencing CSS or JavaScript includes) will not work when the contents are rendered to the web-part page. This is because those references will now be relative to the current page, and thus, most likely, be invalid. The <IFRAME> approach used in this recipe eliminates both the benefits and the drawbacks of the WebRequest approach. The credentials passed to the target page will always be those of the current user; there is no way to preprocess the page before it's displayed, but all relative links and references will remain intact.

Preparation

  1. Create a new C# or VB.NET class library project.

  2. Add a reference to the System.Web .NET assembly.

  3. At the top of the class module, add using or Includes statements for the System.Web.UI.WebControls and System.Web.UI.WebControls.WebParts class libraries.

  4. Open the project properties page, go to the Signing tab, select the Sign the Assembly checkbox, and add a new strong-name key file named something like ConnectablePageViewer.snk.

Process Flow

Process Flow

Note

The process highlighted here pertains to the generic steps required to create a connectable web part, rather than the steps required to create a Page Viewer web part per se.

  1. A .NET interface is like a class, but it contains only property and method signatures without any code. The purpose of the interface in this case is to provide both the provider and consumer web parts with a common data structure to use when data is passed from the provider to the consumer.

  2. Create a provider web-part class. Note that this class will also implement the interface created in step 1.

  3. Override the web-part class's base CreateChildControls() method to add any web controls required to acquire data from the end user. In this case, we need one drop-down list that will contain the list of web sites that may be displayed.

  4. Because the provider web part implements the interface defined in step 1, it must implement any properties in the interface. In our example, we need to implement the Url read-only property, returning the currently selected value of the drop-down list.

  5. A provider web part must have a public method that returns an instance of the provider web part, as viewed through the properties defined in the interface created in step 1. This method is defined to the ASP.NET web-part framework by decorating the method with the ConnectionProvider() attribute.

  6. Create a consumer web-part class.

  7. In the consumer web-part class, override either the CreateChildControls() or RenderContents() base web-part methods, adding code to render the desired HTML to the web-part page.

  8. A consumer web part must have a public method that can receive the data from the provider web part's ConnectionProvider() method—defined in step 5. This method must be decorated with the ConnectionConsumer() attribute to indicate to the ASP.NET web-part framework so that it can receive the data made available by the provider web part. In our example, the ConnectionConsumer() method receives an instance of the provider class, limited by the properties defined for the interface in step 1, which in this case is simply the Url property.

Recipe—VB (See Project ConnectablePageViewerVB, Class ConnectablePageViewerVB.vb)

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Web.UI.WebControls
Imports System.Web.UI.WebControls.WebParts
Namespace ConnectablePageViewerVB
' Step 1: Create the Interface
    ' ----------------------------
    ' The interface is the glue between the "provider" web part
    ' that sends data to the "consumer" web page. It provides
    ' a structure in which to pass the data. In this case
    ' we're just passing a single string value that represents
    ' the URL to display, but we could pass multiple data
    ' items by providing multiple properties
    Public Interface IUrl
        ReadOnly Property Url() As String
    End Interface
    ' Step 2: Create the provider web-part class
    ' ------------------------------------------
    ' The "provider" web part will display a drop-down list
    ' or site-name/URL pairs. When the user selects a value,
    ' the selected URL will be passed to the "consumer".
    Public Class UrlProvider
        Inherits WebPart
        Implements IUrl
        Private ddlUrl As DropDownList = Nothing
        Private _urls As String = _
           "Microsoft;http://www.microsoft.com;Yahoo!;" & _
           "http://www.yahoo.com;Apress;http://www.apress.com"
        ' The "Urls" property will store a semicolon-delimited
        ' list of site-name/URL pairs to populate the drop-down list
        <Personalizable()> _
        <WebBrowsable()> _
        Public Property Urls() As String
            Get
                Return _urls
            End Get
            Set(ByVal value As String)
                _urls = value
            End Set
        End Property
        ' Step 3: Override the "CreateChildControls()" method
        ' ---------------------------------------------------
        ' The CreateChildControls() base method is called
        ' to populate the drop-down list of sites and
        ' add to the web-part output
        Protected Overloads Overrides Sub CreateChildControls()
            MyBase.CreateChildControls()
Try
                ' Create the drop-down list of URLs from
                ' the parsed string in "Urls" property
                Dim arrUrls As String() = _urls.Split(";"c)
                Dim li As ListItem
                ddlUrl = New DropDownList()
                ddlUrl.Items.Add(New ListItem("[Please select a Url]", ""))
                Dim i As Integer = 0
                While i < arrUrls.Length
                    li = New ListItem(arrUrls(i), arrUrls(i + 1))
                    ddlUrl.Items.Add(li)
                    i = i + 2
                End While
                ddlUrl.Items(0).Selected = True
                ddlUrl.AutoPostBack = True
                Me.Controls.Add(ddlUrl)
            Catch ex As Exception
                Dim lbl As New Label()
                lbl.Text = ex.Message
                Me.Controls.Add(lbl)
            End Try
        End Sub
        ' Step 4: Define any methods required by the interface
        ' ----------------------------------------------------
        ' This is the single method that was
        ' specified in the Interface, and must be provided
        ' to pass the selected URL to the "consumer" web
        ' part
        Public ReadOnly Property Url() As String Implements IUrl.Url
            Get
                Return ddlUrl.SelectedValue.ToString()
            End Get
        End Property
        ' Step 5: Define and "decorate" the ConnectionProvider() method
        ' -----------------------------------------------------------
        ' This method is required to wire up the
        ' "provider" with one or more "consumers."
        ' Note the "ConnectionProvider" decoration
        ' that tells .NET to make this the provider's
        ' connection point
        <ConnectionProvider("Url Provider")> _
        Public Function GetUrl() As IUrl
            Return Me
        End Function
    End Class
' Step 6: Define the consumer web-part class
    ' ------------------------------------------
    ' This class defines the "consumer" web part that will
    ' obtain the URL from the "provider"
    Public Class ConnectablePageViewer
        Inherits WebPart
        Private _url As String = ""
        ' Step 7: Override either or both the CreateChildControls() and/or
        ' RenderContents() base methods
        ' ----------------------------------------------------------------
        ' In the RenderContents() method we get the URL value
        ' which has been written to the _url local variable by
        ' the "UrlConsumer()" method that automatically fires
        ' when this web part is wired up with a "provider"
        Protected Overloads Overrides Sub RenderContents( _
           ByVal writer As System.Web.UI.HtmlTextWriter)
            MyBase.RenderContents(writer)
            Try
                If _url <> "" Then
                    ' Create an <IFRAME> HTML tag and set the
                    ' source to the selected url
                    writer.Write("Opening page: " + _url)
                    writer.Write("<hr/>")
                    writer.Write("<div>")
                    writer.Write("<iframe src='" + _url + _
                       "' width='100%' height='800px'></iframe>")
                    writer.Write("</div>")
                Else
                    writer.Write("Please select a Url from the provider.")
                End If
            Catch ex As Exception
                writer.Write(ex.Message)
            End Try
        End Sub
        ' Step 8: Define a ConnectionConsumer() method to receive
        ' data from the provider
        ' -------------------------------------------------------
        ' The UrlConsumer() method is wired up using the
        ' "ConnectionConsumer()" decoration, that tells
        ' .NET to automatically fire this method when
        ' the consumer is connected to a provider
        <ConnectionConsumer("Url Consumer")> _
        Public Sub UrlConsumer(ByVal url As IUrl)
            Try
                _url = url.Url
                ' No op
Catch ex As Exception
            End Try
        End Sub
    End Class
End Namespace

Recipe—C# (See Project ConnectablePageViewerCS, Class ConnectablePageViewerCS.cs)

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
namespace ConnectablePageViewerCS
{
    // Step 1: Create the Interface
    // ----------------------------
    // The interface is the glue between the "provider" web part
    // that sends data to the "consumer" web page.  It provides
    // a structure in which to pass the data.  In this case
    // we're just passing a single string value that represents
    // the URL to display, but we could pass multiple data
    // items by providing multiple properties
    public interface IUrl
    {
        string Url { get; }
    }
    // Step 2: Create the provider web-part class
    // ------------------------------------------
    // The "provider" web part will display a drop-down list
    // or site-name/URL pairs.  When the user selects a value,
    // the selected URL will be passed to the "consumer."
    public class UrlProvider : WebPart, IUrl
    {
        DropDownList ddlUrl = null;
        string _urls =
          "Microsoft;http://www.microsoft.com; " +
          "Yahoo!;http://www.yahoo.com;Apress;http://www.apress.com";
        // The "Urls" property will store a semicolon-delimited
        // list of site-name/URL pairs to populate the drop-down list
        [Personalizable]
        [WebBrowsable]
        public string Urls
        {
            get { return _urls; }
set { _urls = value;  }
        }
        // Step 3: Override the "CreateChildControls()" method
        // ---------------------------------------------------
        // The CreateChildControls() base method is called
        // to populate the drop-down list of sites and
        // add to the web-part output
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            try
            {
                // Create the drop-down list of URLs from
                // the parsed string in "Urls" property
                string[] arrUrls = _urls.Split(';'),
                ListItem li;
                ddlUrl = new DropDownList();
                ddlUrl.Items.Add(new ListItem("[Please select a Url]", ""));
                for (int i = 0; i < arrUrls.Length; i = i + 2)
                {
                    li = new ListItem(arrUrls[i], arrUrls[i + 1]);
                    ddlUrl.Items.Add(li);
                }
                ddlUrl.Items[0].Selected = true;
                ddlUrl.AutoPostBack = true;
                this.Controls.Add(ddlUrl);
            }
            catch (Exception ex)
            {
                Label lbl = new Label();
                lbl.Text = ex.Message;
                this.Controls.Add(lbl);
            }
        }
        // Step 4: Define any methods required by the interface
        // ----------------------------------------------------
        // This is the single method that was
        // specified in the Interface, and must be provided
        // to pass the selected URL to the "consumer" web
        // part
        public string Url
        {
            get { return ddlUrl.SelectedValue; }
        }
// Step 5: Define and "decorate" the ConnectionProvider method
  // -----------------------------------------------------------
  // This method is required to wire up the
        // "provider" with one or more "consumers."
        // Note the "ConnectionProvider" decoration
        // that tells .NET to make this the provider's
        // connection point
        [ConnectionProvider("Url Provider")]
        public IUrl GetUrl()
        {
            return this;
        }
    }
    // Step 6: Define the consumer web-part class
    // ------------------------------------------
    // This class defines the "consumer" web part that will
    // obtain the URL from the "provider"
    public class ConnectablePageViewer : WebPart
    {
        string _url = "";
        // Step 7: Override either or both the CreateChildControls() and/or
        //   RenderContents() base methods
        // ----------------------------------------------------------------
        // In the RenderContents() method, we get the URL value
        // that has been written to the _url local variable by
        // the "UrlConsumer()" method that automatically fires
        // when this web part is wired up with a "provider"
        protected override void RenderContents(
           System.Web.UI.HtmlTextWriter writer)
        {
            base.RenderContents(writer);
            try
            {
                if (_url != "")
                {
                    // Create an <IFRAME> HTML tag and set the
                    // source to the selected URLl
                    writer.Write("Opening page: " + _url);
                    writer.Write("<hr/>");
                    writer.Write("<div>");
                    writer.Write("<iframe src='" + _url +
                       "' width='100%' height=800px'></iframe>");
                    writer.Write("</div>");
                }
else
                {
                    writer.Write("Please select a Url from the provider.");
                }
            }
            catch (Exception ex)
            {
                writer.Write(ex.Message);
            }
        }
        // Step 8: Define a ConnectionConsumer() method to receive
        // data from the provider
        // -------------------------------------------------------
        // The UrlConsumer() method is wired up using the
        // "ConnectionConsumer()" decoration that tells
        // .NET to automatically fire this method when
        // the consumer is connected to a provider
        [ConnectionConsumer("Url Consumer")]
        public void UrlConsumer(IUrl url)
        {
            try
            {
                _url = url.Url;
            }
            catch (Exception ex)
            {
                // No op
            }
        }
    }
}

To Run

Note

Please see the "To Run" section of Recipe 4-1 for instructions on how to deploy a web part to a single site collection. After the web part has been successfully deployed, proceed with the following steps.

Open a web-part page in the site collection where you have just deployed your two web parts, and then add both web parts to the page as shown in Figure 4-9.

The connectable Page Viewer waiting for the user to select a URL

Figure 4.9. The connectable Page Viewer waiting for the user to select a URL

Note that because no URL has yet been selected, the Connectable Page Viewer web part displays the message "Please select a URL from the provider." Before the Page Viewer part will recognize that a URL has been selected, however, you must connect the web parts. To do so, choose the Site Actions

The connectable Page Viewer waiting for the user to select a URL
Connecting the connectable Page Viewer to the URL source

Figure 4.10. Connecting the connectable Page Viewer to the URL source

Finally, select a site name from the URL provider web part to display the corresponding site in the Connectable Page Viewer, as shown in Figure 4-11.

The Connectable Page Viewer in action

Figure 4.11. The Connectable Page Viewer in action

Related Recipes

Reading Web-Part Parameters from the Querystring

There are many scenarios in which it would be useful for one or more web parts on a page to display differently depending on what, if any, parameters are included in the URL querystring. For example, you might wish to create a single web-part page to display client information, and pass the client ID as a parameter. That way, you can have a single web-part page that serves up information for thousands of clients.

In this recipe, you'll create a variation of the XML web part that will check the querystring for a client ID and use that to filter data from a SharePoint list, to format and display data for the specified client.

Recipe Type: Web Part

Ingredients

Assembly References

  • Windows.SharePoint.Services .NET assembly

  • System.Web .NET assembly

Special Considerations

  • Note that, unlike most web-part recipes in this chapter, the Querystring web part does require access to the SharePoint object model. The reason is that we are using a SharePoint list as our data source. If you try one of the variations that use a non-SharePoint data source, the reference to the Windows.SharePoint.Services assembly will not be required.

Preparation

  1. Create a custom list called Clients as described in the "To Run" section.

  2. Create a new C# or VB.NET class library.

  3. Add references to the Windows.SharePoint.Services and System.Web .NET assemblies.

  4. Add using or Includes (depending on language used) statements for the following:

    • Microsoft.SharePoint

    • Microsoft.SharePoint.WebControls

    • System.Web.UI.WebControls

    • System.Web.UI.WebControls.WebParts

    • System.Data

  5. Add the properties specified in the following source code.

  6. Override the RenderContents() base web part method.

  7. Add the custom displayClientData() method.

  8. Create an XML transform (XSLT) to format the resulting data.

Process Flow

Process Flow
  1. If the Debug checkbox is selected, loop through each querystring parameter and write to the web-part page.

  2. Write values of web-part properties that determine output format and (if format is XSLT) the XSLT transform file to use.

  3. Instantiate an SPWeb object representing the web site that the current web-part page is a member of.

  4. Get a handle to the Clients list—assuming one exists.

  5. Use the SPListItems.GetDataTable() method to write the entire contents of the list into an ADO.NET DataTable for easier processing.

  6. Use an ADO.NET DataView object to filter the DataTable created in step 4 based on the client ID passed in the querystring.

  7. Determine whether the web part has been set to format data by using a DataGrid or XSLT.

  8. If the web part will use a DataGrid, create a new DataGrid object, assign the DataView object created in step 6 to its DataSource property, and then render the DataGrid to the page.

  9. Otherwise, create a new XML transform web control, set its content to an XML representation of the DataView's data (by way of a DataSet and the DataView.ToTable() method). Set the transform path to that provided by the web part's XSLT path property, and render the XML to the page.

Recipe—VB (See Project QuerystringWebPartVB, Class QuerystringWebPart.vb)

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Web.UI.WebControls
Imports System.Web.UI.WebControls.WebParts
Imports Microsoft.SharePoint
Imports Microsoft.SharePoint.WebControls
Imports System.Data
Public Class QueryStringWebPartVB
    Inherits WebPart
    ' Define local variables
    Private _debug As Boolean = False
    Private _formatUsing As enumFormatUsing = enumFormatUsing.DataGrid
    Private _xsltPath As String = ""
    'ENUM types will result in drop-down lists in
    'the web-part property sheet
    Public Enum enumFormatUsing
        DataGrid = 1
        XSLT = 2
    End Enum
' Display debug info?
    <Personalizable()> _
    <WebBrowsable()> _
    <WebDisplayName("Debug?")> _
    <WebDescription("Check to cause debug information to be displayed")> _
    Public Property Debug() As Boolean
        Get
            Return _debug
        End Get
        Set(ByVal value As Boolean)
            _debug = value
        End Set
    End Property
    'Create property to determine whether DataGrid or
    'XSLT should be used to format output
    <Personalizable(PersonalizationScope.[Shared]), _
        WebBrowsable(), WebDisplayName("Format Using:"), _
        WebDescription("What method do you want to use " & _
        "to format the results.")> _
    Public Property FormatUsing() As enumFormatUsing
        Get
            Return _formatUsing
        End Get
        Set(ByVal value As enumFormatUsing)
            _formatUsing = value
        End Set
    End Property
    'If XSLT will be used, this property specifies
    'its server-relative path
    <Personalizable(PersonalizationScope.[Shared]), _
        WebBrowsable(), WebDisplayName("XSLT Path:"), _
        WebDescription("If formatting with XSLT, " & _
        "provide full path to XSLT document.")> _
    Public Property XSLTPath() As String
        Get
            Return _xsltPath
        End Get
        Set(ByVal value As String)
            _xsltPath = value
        End Set
    End Property
    Protected Overloads Overrides Sub RenderContents(ByVal writer As _
       System.Web.UI.HtmlTextWriter)
        MyBase.RenderContents(writer)
Try
            Dim qs As System.Collections.Specialized.NameValueCollection = _
               Page.Request.QueryString
            If _debug Then
                ' Step 1: Parse the querystring and display
                If qs.Count > 0 Then
                    writer.Write("<strong>Querystring parameters: </strong>")
                    writer.Write("<blockquote>")
                    For i As Integer = 0 To qs.Count - 1
                        writer.Write(qs.Keys(i) + " = " + qs(i) + "<br/>")
                    Next
                    writer.Write("</blockquote>")
                Else
                    writer.Write("No querystring parameters exist<br/>")
                End If
                ' Step 2: Display web-part property values
                writer.Write("<strong>Format output using:</strong> " + _
                  _formatUsing.ToString() + "<br/>")
                writer.Write("<strong>XSLT path:</strong> " + _
                  _xsltPath.ToString() + "<br/>")
                writer.Write("<hr/>")
            End If
            ' Step 3: Display items from Client list based on provided ID
            Dim clientId As String = qs("clientId")
            If clientId IsNot Nothing Then
                displayClientData(clientId, writer)
            Else
                writer.Write("Client ID was not provided in querystring")
            End If
        Catch e As Exception
            writer.Write("<font color='red'>" + e.Message + "</font>")
        End Try
    End Sub
    Private Sub displayClientData(ByVal clientId As String, _
       ByVal writer As System.Web.UI.HtmlTextWriter)
        Try
            ' Step 4: Get handle to current web site and client list
            Dim web As SPWeb = SPControl.GetContextWeb(Context)
            Dim clients As SPList = web.Lists("Clients")
            ' Step 5: Copy clients' data into a DataTable object
            ' for easier manipulation
            Dim dsClients As New DataSet("Clients")
            Dim dtClients As DataTable = clients.Items.GetDataTable()
            dtClients.TableName = "Clients"
' Step 6: Filter for the specified client ID
            Dim dvClients As New DataView()
            dvClients.Table = dtClients
            dvClients.RowFilter = "ClientId = '" + clientId + "'"
            ' Step 7: Determine display mechanism to use
            If FormatUsing = enumFormatUsing.DataGrid Then
                ' Step 8: Display as DataGrid
                Dim dgClients As New DataGrid()
                dgClients.DataSource = dvClients
                dgClients.DataBind()
                dgClients.RenderControl(writer)
            Else
                ' Step 9: Format using provided XSLT
                Dim xml As New System.Web.UI.WebControls.Xml()
                dsClients.Tables.Add(dvClients.ToTable("Clients"))
                xml.DocumentContent = dsClients.GetXml()
                xml.TransformSource = XSLTPath
                xml.RenderControl(writer)
            End If
        Catch ex As Exception
            ' If error occurs, notify end-user
            writer.WriteLine("<font color='red'><strong>" + _
               ex.Message + "</font>")
        End Try
    End Sub
End Class

Recipe—C# (See Project QuerystringWebPartCS, Class QuerystringWebPart.cs)

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Data;
namespace QuerystringWebPartCS
{
    public class QueryStringWebPartCS : WebPart
    {
        // Define local variables
        bool _debug = false;
        enumFormatUsing _formatUsing = enumFormatUsing.DataGrid;
        string _xsltPath = "";
        //ENUM types will result in drop-down lists in
        //the web-part property sheet
public enum enumFormatUsing
        {
            DataGrid = 1,
            XSLT = 2
        }
        // Display debug info?
        [Personalizable]
        [WebBrowsable]
        [WebDisplayName("Debug?")]
        [WebDescription("Check to cause debug information to be displayed")]
        public bool Debug
        {
            get { return _debug; }
            set { _debug = value; }
        }
        //Create property to determine whether DataGrid or
        //XSLT should be used to format output
        [Personalizable(PersonalizationScope.Shared), WebBrowsable(),
            WebDisplayName("Format Using:"),
            WebDescription("What method do you want " +
                "to use to format the results.")]
        public enumFormatUsing FormatUsing
        {
            get { return _formatUsing; }
            set { _formatUsing = value; }
        }
        //If XSLT will be used, this property specifies
        //its server-relative path
        [Personalizable(PersonalizationScope.Shared),
            WebBrowsable(), WebDisplayName("XSLT Path:"),
            WebDescription("If formatting with XSLT, " +
               "provide full path to XSLT document.")]
        public string XSLTPath
        {
            get { return _xsltPath; }
            set { _xsltPath = value; }
        }
        protected override void RenderContents(
           System.Web.UI.HtmlTextWriter writer)
        {
            base.RenderContents(writer);
            try
            {
                System.Collections.Specialized.NameValueCollection qs =
                   Page.Request.QueryString;
if (_debug)
                {
                    // Step 1: Parse the querystring and display
                    if (qs.Count > 0)
                    {
                        writer.Write(
                           "<strong>Querystring parameters: </strong>");
                        writer.Write("<blockquote>");
                        for (int i = 0; i < qs.Count; i++)
                        {
                            writer.Write(qs.Keys[i] + " = " + qs[i] +
                               "<br/>");
                        }
                        writer.Write("</blockquote>");
                    }
                    else
                    {
                        writer.Write("No querystring parameters exist<br/>");
                    }
                    // Step 2: Display web-part property values
                    writer.Write("<strong>Format output using:</strong> " +
                       _formatUsing + "<br/>");
                    writer.Write("<strong>XSLT path:</strong> " +
                      _xsltPath + "<br/>");
                    writer.Write("<hr/>");
                }
                // Step 3: Display items from Client list
                // based on provided ID
                string clientId = qs["clientId"];
                if (clientId != null)
                {
                    displayClientData(clientId, writer);
                }
                else
                {
                    writer.Write(
                       "Client ID was not provided in querystring");
                }
            }
            catch (Exception e)
            {
                writer.Write("<font color='red'>" + e.Message + "</font>");
            }
        }
private void displayClientData(
           string clientId, System.Web.UI.HtmlTextWriter writer)
        {
            try
            {
                // Step 4: Get handle to current web site and client list
                SPWeb web = SPControl.GetContextWeb(Context);
                SPList clients = web.Lists["Clients"];
                // Step 5: Copy clients' data into a DataTable object
                // for easier manipulation
                DataSet dsClients = new DataSet("Clients");
                DataTable dtClients = clients.Items.GetDataTable();
                dtClients.TableName = "Clients";
                // Step 6: Filter for the specified client ID
                DataView dvClients = new DataView();
                dvClients.Table = dtClients;
                dvClients.RowFilter = "ClientId = '" + clientId + "'";
                // Step 7: Determine display mechanism to use
                if (FormatUsing == enumFormatUsing.DataGrid)
                {
                    // Step 8: Display as DataGrid
                    DataGrid dgClients = new DataGrid();
                    dgClients.DataSource = dvClients;
                    dgClients.DataBind();
                    dgClients.RenderControl(writer);
                }
                else
                {
                    // Step 9: Format using provided XSLT
                    System.Web.UI.WebControls.Xml xml =
                       new System.Web.UI.WebControls.Xml();
                    dsClients.Tables.Add(dvClients.ToTable("Clients"));
                    xml.DocumentContent = dsClients.GetXml();
                    xml.TransformSource = XSLTPath;
                    xml.RenderControl(writer);
                }
            }
            catch (Exception ex)
            {
                // If error occurs, notify end user
                writer.WriteLine("<font color='red'><strong>" +
                   ex.Message + "</font>");
            }
        }
    }
}

Recipe—Clients.xslt (See Project QuerystringWebPartCS, File Clients.xslt)

This is the XML transform used to format the XML representation of the selected client data as HTML on the page. This XSLT simply builds a well-formed HTML <TABLE> from the XML provided by the DataSet.

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:template match="/">
          <!-- Display each matching client -->
          <table cellpadding="10" border="1">
          <xsl:for-each select="Clients/Clients">
               <tr>
                    <td>
                         Client Id:
                    </td>
                    <td>
                         <strong>
                              <xsl:value-of select="ClientId"/>
                         </strong>
                    </td>
               </tr>
               <tr>
                    <td>
                         Client Name:
                    </td>
                    <td>
                         <strong>
                              <xsl:value-of select="ClientName"/>
                         </strong>
                    </td>
               </tr>
               <tr>
                    <td>
                         Address:
                    </td>
                    <td>
                         <strong>
                              <xsl:value-of select="Address"/>
                         </strong>
                    </td>
               </tr>
          </xsl:for-each>
          </table>
     </xsl:template>
</xsl:stylesheet>

To Run

Note

Please see the "To Run" section of Recipe 4-1 for instructions on how to deploy a web part to a single site collection. After the web part has been successfully deployed, proceed with the following steps.

The next step is to create a new list called Clients that includes three fields: ClientId, ClientName, and Address (actually, the only field the web part requires is ClientId, but the sample XSLT shown in the preceding section assumes all three fields will be in the XML output of the DataSet).

After you have created the Clients list, add some rows as shown in Figure 4-12.

Client data to use with the Querystring web part

Figure 4.12. Client data to use with the Querystring web part

After the Clients list has been created and a few rows have been added, navigate to a web-part page in the same site and add the Querystring web part. Initially, you will receive an error message indicating that a client ID was not provided in the querystring. To remedy this, simply add some text such as ?clientid=1001 to the end of the URL in the browser's location field. Figure 4-13 shows the Querystring web part using custom XSLT to format the output.

Note

Any querystring is always preceded by the question mark (?) character, whereas parameters within a querystring are separated with an ampersand (&) character. So if clientid is the first (or only) parameter, it will be preceded by a ?. If it appears after one or more other parameters, it will be preceded by the & character.

The Querystring web part using XSLT to format the results

Figure 4.13. The Querystring web part using XSLT to format the results

Variations

  • The preceding recipe always assumes that the parameter that will provide the filter data is called ClientId, that the list that holds the source data is named Clients, and that the field to compare to is named ClientId, just like the querystring parameter. Adding three additional web-part properties to contain the name of the querystring parameter, the list containing source records, and the field in that list to compare against the querystring parameter will allow this web part to address a virtually unlimited number of applications where you need to filter data in a list based on the querystring.

  • Modify this recipe to obtain its data from an XML document source or a SQL query.

Using the SmartPart to Expose a .NET User Control

You're probably already aware of the wide range of freeware, shareware, and commercial web parts that are available. One that deserves special attention is the SmartPart by Jan Tielens. This part does something very elegant and, at least conceptually, simple. SmartPart makes it possible to use standard ASP.NET user controls as web parts. The reason this is useful is that while Visual Studio doesn't provide a What You See Is What You Get (WYSIWYG) design surface for web parts (which are really just a special type of server control), VS does for user controls. This means that, by using the SmartPart, you can lay out your web parts visually and assign complex event handling if necessary. It's pretty cool!

In this recipe, you'll create and deploy a web part that your colleagues can use to tell each other how busy they are, indicating their workload by setting a status to one of the following: green = need more work, yellow = pretty busy but will be available for more soon, or red = overloaded, don't bother me.

The status will be stored in a shared custom list, with an entry for each user.

Recipe Type: Web Part

Ingredients

Classes Used

  • SPSite class

  • SPWeb class

  • SPList class

  • SPListItem class

Other Ingredients

  • Three .ico files that ship with Microsoft development environments: Trffc10a.ico, Trffc10b.ico, and Trffc10c.ico. These three files should be placed in SharePoint's image folder.

Special Considerations

  • Any custom assemblies that your user control requires must be installed to either the in folder of the target web application or to the GAC.

Preparation

  1. Download and install the SmartPart, which as of this writing is available at www.codeplex.com/smartpart/Release/ProjectReleases.aspx?ReleaseId=10697.

  2. Create a UserControls folder under your target SharePoint web application folder. For example, if you are using the standard web application on port 80, create a new folder at C:InetpubwwwrootwssVirtualDirectories80UserControls.

  3. Create a new C# or VB.NET ASP.NET web application project.

  4. Add a user control to the project and name it something like SmartPartStatusWebPart.ascx.

  5. Add a reference to the Windows.SharePoint.Services .NET assembly.

  6. Add using or Includes statements for the Microsoft.SharePoint and Microsoft.SharePoint.WebControls namespaces.

  7. Copy the three icon files to the SharePoint images folder, which is typically at C:Program FilesCommon FilesMicrosoft Sharedweb server extensions12TEMPLATEIMAGES.

  8. Create a new SharePoint custom list named Status with two text fields: UserAlias and Status. Make sure that all users have Contribute rights to this list.

Process Flow: GetProfileStatus()

Process Flow: GetProfileStatus()
  1. Instantiate SPSite, SPWeb, and SPList objects to obtain a handle to the list.

  2. Step through the list of items to find the one that matches the current user alias. For large lists, use an SPQuery object for better performance.

  3. If a match is found, return the associated value of the Status column and exit.

  4. Otherwise, if no match is found, return an empty string.

Process Flow: UpdateStatus()

Process Flow: UpdateStatus()
  1. Instantiate SPSite, SPWeb, and SPList objects to obtain a handle to the list.

  2. Step through the list of items to find the one that matches the current user alias. For large lists, use an SPQuery object for better performance.

  3. If a match is found, update the Status field with the color value of the currently selected radio button.

  4. If no match is found, this must be the first time the user has set their status, so insert a new item into the list and set the user alias and status accordingly.

Recipe—Page Layout (See Project SmartPartStatusWebPartCS, File SmartPartStatusWebPart.ascx)

Unlike standard web parts, where all UI elements are defined in the source code, an ASP.NET user control includes design surface elements as well as code. The following screen shot displays the elements of the layout. The radio buttons are named radRed, radYellow, and radGreen, respectively. The images are imgRed, imgYellow, and imgGreen. Figure 4-14 shows the user control displayed in Visual Studio's design surface.

The .ascx page shown in design view

Figure 4.14. The .ascx page shown in design view

Note

All of the preceding radio buttons need to be given the same GroupName property, and their AutoPostBack property needs to be set to true.

Recipe—VB (See Project SmartPartStatusWebPartCS, Code-Behind SmartPartStatusWebPart.ascx.vb)

Note

Be sure that the variables _statusListSiteUrl, _statusListWeb, and _statusList reflect the actual location of the list that will contain user status information.

Imports Microsoft.SharePoint
Imports Microsoft.SharePoint.WebControls
Partial Class SmartPartStatusWebPart
    Inherits System.Web.UI.UserControl
    'Define local variables
    Private _showImage As Boolean = True
    Private _statusListSiteUrl As String = "http://localhost"
    Private _statusListWeb As String = "spwp"
    Private _statusList As String = "Status"
    Private Sub Page_PreRender(ByVal sender As Object, _
       ByVal e As System.EventArgs) Handles MyBase.PreRender
        'Only run this code if this is the first time the
        'user control has been displayed on the current
        'page since it was opened
If Not IsPostBack Then
            HideAllImages()
            Select Case GetProfileStatus()
                Case "Green"
                    Me.imgGreen.Visible = True
                    Me.radGreen.Checked = True
                Case "Yellow"
                    Me.imgYellow.Visible = True
                    Me.radYellow.Checked = True
                Case "Red"
                    Me.imgRed.Visible = True
                    Me.radRed.Checked = True
                Case Else
            End Select
        End If
    End Sub
    Private Function GetProfileStatus() As String
        Try
            'Step 1: Define necessary objects
            Dim site As SPSite = New SPSite(_statusListSiteUrl)
            Dim web As SPWeb = site.AllWebs(_statusListWeb)
            Dim list As SPList = web.Lists(_statusList)
            'Step 2: Find the list item for the current user, and update its status
            'web.AllowUnsafeUpdates = True
            For Each ListItem As SPListItem In list.Items
                Try
                    'Step 3: If user is found, return their status
                    If ListItem("UserAlias").ToString.ToLower = _
                            Context.User.Identity.Name.ToLower Then
                        Return ListItem("Status")
                        Exit For
                    End If
                Catch ex As Exception
                    'No op
                End Try
            Next
            web.Dispose()
            site.Dispose()
        Catch ex As Exception
            'No op
        End Try
        'Step 4: If we got this far, no entry was found for the current user
        Return ""
    End Function
Private Sub UpdateStatus()
        Try
            'Step 1: Get a handle to the list that we're using to store
            'user status information
            Dim site As SPSite = New SPSite(_statusListSiteUrl)
            Dim web As SPWeb = site.AllWebs(_statusListWeb)
            Dim list As SPList = web.Lists(_statusList)
            Dim listItem As SPListItem
            Dim boolFound As Boolean = False
            'Step 2: Find the list item for the current user, and update its status
            For Each listItem In list.Items
                Try
                    'Step 3: If found, update the user's status
                    If listItem("UserAlias").ToString.ToLower = _
                        Context.User.Identity.Name.ToLower Then
                        listItem("Status") = GetUserControlStatus()
                        listItem.Update()
                        boolFound = True
                        Exit For
                    End If
                Catch ex As Exception
                End Try
            Next
            'Step 4: If an entry for the current user wasn't found in the list,
            'add one now.
            If Not boolFound Then
                listItem = list.Items.Add()
                listItem("UserAlias") = Context.User.Identity.Name
                listItem("Status") = GetUserControlStatus()
                listItem.Update()
            End If
            web.Dispose()
            site.Dispose()
        Catch ex As Exception
            Dim lbl As New Label
            lbl.Text = "<font color='red'>" & ex.Message & "</font><br/>"
            Me.Controls.Add(lbl)
        End Try
    End Sub
    'Get the currently selected status from
    'the user control UI
    Private Function GetUserControlStatus() As String
        If radRed.Checked Then
            Return "Red"
        ElseIf radYellow.Checked Then
            Return "Yellow"
Else
            Return "Green"
        End If
    End Function
    'Helper function to make sure all images are
    'hidden prior to displaying the selected one
    Public Sub HideAllImages()
        Me.imgGreen.Visible = False
        Me.imgYellow.Visible = False
        Me.imgRed.Visible = False
    End Sub
    'The following event handlers process button clicks to
    'display the image corresponding to the selected status
    Public Sub radGreen_CheckedChanged(ByVal sender As System.Object, _
       ByVal e As System.EventArgs) Handles radGreen.CheckedChanged
        HideAllImages()
        If radGreen.Checked Then
            If _showImage Then
                imgGreen.Visible = True
            End If
        End If
        UpdateStatus()
    End Sub
    Public Sub radYellow_CheckedChanged(ByVal sender As System.Object, _
       ByVal e As System.EventArgs) Handles radYellow.CheckedChanged
        HideAllImages()
        If radYellow.Checked Then
            If _showImage Then
                imgYellow.Visible = True
            End If
        End If
        UpdateStatus()
    End Sub
    Public Sub radRed_CheckedChanged(ByVal sender As System.Object, _
       ByVal e As System.EventArgs) Handles radRed.CheckedChanged
        HideAllImages()
        If radRed.Checked Then
            If _showImage Then
                imgRed.Visible = True
            End If
        End If
        UpdateStatus()
    End Sub
End Class

Recipe—C# (See Project SmartPartStatusWebPartCS, Code-Behind SmartPartStatusWebPartCS.ascx.cs)

Note

Be sure that the variable _statusListSiteUrl, _statusListWeb, and _statusList reflect the actual location of the list that will contain user status information.

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
partial class SmartPartStatusWebPartCS : System.Web.UI.UserControl
{
    //Define local variables
    private bool _showImage = true;
    private string _statusListSiteUrl = "http://localhost";
    private string _statusListWeb = "spwp";
    private string _statusList = "Status";
    private void Page_PreRender(object sender, System.EventArgs e)
    {
        //Only run this code if this is the first time the
        //user control has been displayed on the current
        //page since it was opened
        if (!IsPostBack)
        {
            HideAllImages();
            switch (GetProfileStatus())
            {
                case "Green":
                    this.imgGreen.Visible = true;
                    this.radGreen.Checked = true;
                    break;
                case "Yellow":
                    this.imgYellow.Visible = true;
                    this.radYellow.Checked = true;
                    break;
case "Red":
                    this.imgRed.Visible = true;
                    this.radRed.Checked = true;
                    break;
                default:
                    break;
            }
        }
    }
    private string GetProfileStatus()
    {
        try
        {
            //Step 1: Define necessary objects
            SPSite site = new SPSite(_statusListSiteUrl);
            SPWeb web = site.AllWebs[_statusListWeb];
            SPList list = web.Lists[_statusList];
            //Step 2: Find the list item for the current user, and update its status
            //web.AllowUnsafeUpdates = True
            foreach (SPListItem ListItem in list.Items)
            {
                try
                {
                    //Step 3: If found, return their status
                    if (ListItem["UserAlias"].ToString().ToLower() ==
                       Context.User.Identity.Name.ToLower())
                    {
                        return ListItem["Status"].ToString();
                    }
                }
                catch (Exception ex)
                {
                    //No op
                }
            }
            web.Dispose();
            site.Dispose();
        }
        catch (Exception ex)
        {
            //No op
        }
        //Step 4: If we got this far, no entry was found for the current user
        return "";
    }
private void UpdateStatus()
    {
        try {
            //Step 1: Get a handle to the list that we're using to store
            //user status information
            SPSite site = new SPSite(_statusListSiteUrl);
            SPWeb web = site.AllWebs[_statusListWeb];
            SPList list = web.Lists[_statusList];
            SPListItem listItem;
            bool boolFound = false;
            //Step 2: Find the list item for the current user, and update its status
            foreach (SPListItem li in list.Items) {
                try {
                    //Step 3: If found, set their status
                    if (li["UserAlias"].ToString().ToLower() ==
                       Context.User.Identity.Name.ToLower()) {
                        li["Status"] = GetUserControlStatus();
                        li.Update();
                        boolFound = true;
                        break;
                    }
                }
                catch (Exception ex) {
                }
            }
            //Step 4: If an entry for the current user wasn't found in the list,
            //add one now.
            if (!boolFound) {
                listItem = list.Items.Add();
                listItem["UserAlias"] = Context.User.Identity.Name;
                listItem["Status"] = GetUserControlStatus();
                listItem.Update();
            }
            web.Dispose();
            site.Dispose();
        }
        catch (Exception ex) {
            Label lbl = new Label();
            lbl.Text = "<font color='red'>" + ex.Message + "</font><br/>";
            this.Controls.Add(lbl);
        }
    }
//Get the currently selected status from
    //the user control UI
    private string GetUserControlStatus()
    {
        if (radRed.Checked)
        {
            return "Red";
        }
        else if (radYellow.Checked)
        {
            return "Yellow";
        }
        else
        {
            return "Green";
        }
    }
    //Helper function to make sure all images are
    //hidden prior to displaying the selected one
    public void HideAllImages()
    {
        this.imgGreen.Visible = false;
        this.imgYellow.Visible = false;
        this.imgRed.Visible = false;
    }
    //The following event handlers process button clicks to
    //display the image corresponding to the selected status
    public void radGreen_CheckedChanged(object sender, EventArgs e)
    {
        HideAllImages();
        if (radGreen.Checked)
        {
            if (_showImage)
            {
                imgGreen.Visible = true;
            }
        }
        UpdateStatus();
    }
    public void radYellow_CheckedChanged(object sender, EventArgs e)
    {
        HideAllImages();
        if (radYellow.Checked)
        {
            if (_showImage)
            {
                imgYellow.Visible = true;
}
        }
        UpdateStatus();
    }
    public void radRed_CheckedChanged(object sender, EventArgs e)
    {
        HideAllImages();
        if (radRed.Checked)
        {
            if (_showImage)
            {
                imgRed.Visible = true;
            }
        }
        UpdateStatus();
    }
}

To Run

After you have successfully compiled your ASP.NET application containing the status user control, copy the user control .ascx and code-behind files (either .ascx.cs or .ascx.vb depending on the language you're using) to the UserControls folder you created earlier.

Next, navigate to a web-part page and place a SmartPart web part on the page. Open the SmartPart's property sheet and select the user control from the drop-down list of user controls in the UserControl folder. That's all there is to it!

When the status control is displayed for a given user for the first time, no image will be displayed. Clicking on one of the radio buttons will cause a new entry for the current user to be inserted into the status list. From that point forward, the Status web part will "remember" the user's status by looking up the user's entry in the status list. Figure 4-15 shows the Status web part as it will display on a web-part page.

The Status web part in action

Figure 4.15. The Status web part in action

Creating a ZoneTab Web Part

There must be some deep-seated, primal reason why end users love tabs! Not sure why, but they do. In this recipe, you'll learn how to create a web part that displays one to six tabs in a web-part page zone, and enables the user to configure which web parts to display when a particular tab is selected.

This recipe demonstrates several interesting techniques, including these:

  • Programmatically hiding or unhiding web parts on a web-part page

  • Creating event handlers that execute custom code when a control that belongs to a web part fires an event

  • Dynamically modifying attributes of controls when an event occurs

Because there's so much happening in this recipe, it's a bit longer than most of those you'll find in this book, but I'm confident it will be worth the extra work!

Recipe Type: Web Part

Ingredients

Assembly References

  • System.Web .NET assembly

  • System.Drawing .NET assembly

Special Considerations

  • Because this web part uses the generic ASP.NET 2.0 web-part framework and doesn't need to communicate directly with SharePoint, we won't add a reference to the Windows.SharePoint.Services library. However, if you want to use the legacy SharePoint web-part framework, you will need to add that reference to the project.

Preparation

  1. Create a new C# or VB.NET class library.

  2. Add references to the System.Web and System.Drawing .NET assemblies.

  3. On the project properties Signing tab, select the Sign the Assembly checkbox and specify a new strong-name key file.

  4. Add public properties as shown in the following source code.

  5. Override the CreateChildControls() base web-part method.

  6. Create an event handler for the tab Click events.

  7. Create the ShowHideWebParts() custom method as shown in the following source code.

  8. Override the RenderContents() method to call the ShowHideWebParts() method.

Process Flow: CreateChildControls()

Process Flow: CreateChildControls()

The first of the two most interesting processes in this recipe is the overridden CreateChildControls() method that is responsible for drawing the tab menu.

  1. Create an in-memory representation of an HTML <TABLE> element, and a single <TR> element that will contain our tabs.

  2. Loop through the list of tabs and their associated named web parts. Tabs are designated by a leading asterisk (*) in the _tabData variable.

  3. Add a space between tabs.

  4. Add a new <TD> (cell) object to hold the tab, and assign it a unique ID so we can reference it later in our code.

  5. Add a new <A> (hyperlink) object that the user can click to display all named web parts associated with a tab. Give the hyperlink a unique ID as well so we can also reference it later in the code.

  6. Attach an event handler to the hyperlink object (specifically, it's an ASP.NET LinkButton control) that will fire when the user clicks the hyperlink.

  7. Finish setting properties that will affect how each tab displays.

  8. Add the <A> hyperlink object to the <TD> table cell object.

  9. Add the <TD> cell to the <TR> table row object.

  10. When all tabs have been added to the <TR> object, add that to the <TABLE> object and render the entire <TABLE> to the page.

Process Flow: ShowHideWebParts()

Process Flow: ShowHideWebParts()

The second interesting method is ShowHideWebParts(). This is a custom method that loops through all web parts in the current zone that are below the ZoneTab web part, and hides those that are not associated with the currently selected tab.

1. If no tab has yet been selected, select the first (leftmost) tab.
2. Get a handle to the collection of all web parts in the same zone as the ZoneTab web part and hide any web parts below the ZoneTab.
3. Loop through the collection of tabs and associated named web parts.
4.–7. When the selected tab is found, unhide any associated web parts.
8. Bring the selected tab to the "front" by changing the border of the associated <TD> table cell element.
9. Highlight the <A> hyperlink element.
10. If it isn't the selected tab, move it to the "back" by changing the associated <TD> table cell element.
11. Unhighlight the tab.

Recipe—VB (See Project ZoneTabWebPartVB, Class ZoneTabWebPart.vb)

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Web.UI.WebControls
Imports System.Web.UI.WebControls.WebParts
Imports System.Drawing
Namespace ZoneTabWebPartVB
    Public Class ZoneTabWebPart
        Inherits WebPart
        ' Local variables
        Private _tabData As String = ""
        Private _debug As Boolean = False
        Private _selectedTab As String
        Private _tabWidth As Integer = 100
        Private _tabBackgroundColorSelected As String = "white"
        Private _tabBackgroundColorDeselected As String = "whitesmoke"
        <Personalizable()> _
        <WebBrowsable()> _
        <WebDisplayName( _
        "Flag indicating whether debug info should be displayed")> _
        Public Property Debug() As Boolean
            Get
                Return _debug
            End Get
            Set(ByVal value As Boolean)
                _debug = value
            End Set
        End Property
        ' String containing semicolon-delimited list
        ' of tab names. Tab names are preceeded by "*".
        '
        ' Example: *Tab 1;webpart1;webpart2;*Tab 2;webpart3
        '
<Personalizable()> _
        <WebBrowsable()> _
        <WebDisplayName( _
        "A delimited list of tab names and associated web parts")> _
        Public Property TabData() As String
            Get
                Return _tabData
            End Get
            Set(ByVal value As String)
                _tabData = value
            End Set
        End Property
        <Personalizable()> _
        <WebBrowsable()> _
        <WebDescription("Color of selected tab")> _
        Public Property SelectedColor() As String
            Get
                Return _tabBackgroundColorSelected
            End Get
            Set(ByVal value As String)
                _tabBackgroundColorSelected = value
            End Set
        End Property
        <Personalizable()> _
        <WebBrowsable()> _
        <WebDescription("Color of un-selected tab")> _
        Public Property DeSelectedColor() As String
            Get
                Return _tabBackgroundColorDeselected
            End Get
            Set(ByVal value As String)
                _tabBackgroundColorDeselected = value
            End Set
        End Property
        <Personalizable()> _
        <WebBrowsable()> _
        <WebDisplayName("Width in pixels for each tab")> _
        Public Property TabWidth() As Integer
            Get
                Return _tabWidth
            End Get
            Set(ByVal value As Integer)
                _tabWidth = value
            End Set
        End Property
' Add tab-links to page
        Protected Overloads Overrides Sub CreateChildControls()
            MyBase.CreateChildControls()
            Try
                Dim arrTabs As String() = _tabData.Split(";"c)
                ' Build list of tabs in the form
                ' of an HTML <TABLE> with <A> tags
                ' for each tab
                ' Step 1: Define <TABLE> and <TR> HTML elements
                Dim tbl As New Table()
                tbl.CellPadding = 0
                tbl.CellSpacing = 0
                Dim tr As New TableRow()
                tr.HorizontalAlign = HorizontalAlign.Left
                ' Step 2: Loop through list of tabs, adding
                ' <TD> and <A> HTML elements for each
                Dim tc As TableCell
                Dim tab As LinkButton
                Dim tabCount As Integer = 0
                For i As Integer = 0 To arrTabs.Length - 1
                    If arrTabs(i).IndexOf("*") = 0 Then
                        ' Step 3: Add a blank separator cell
                        tc = New TableCell()
                        tc.Text = "&nbsp;"
                        tc.Width = _
                           System.Web.UI.WebControls.Unit.Percentage(1)
                        tc.Style("border-bottom") = "black 1px solid"
                        tr.Cells.Add(tc)
                        ' Step 4: Create a <TD> HTML element to hold the tab
                        tc = New TableCell()
                        tc.ID = "tc_" + _
                           arrTabs(i).Substring(1).Replace(" ", "_")
                        tc.Width = _
                           System.Web.UI.WebControls.Unit.Pixel(_tabWidth)
                        ' Step 5: Create an <A> HTML element to represent
                        ' the tab. Discard first character, which
                        ' was a "*"
                        tab = New LinkButton()
                        tab.ID = "tab_" + _
                           arrTabs(i).Substring(1).Replace(" ", "_")
                        tab.Text = arrTabs(i).Substring(1)
                        ' Step 6: Attach event handler that will execute when
                        ' user clicks on tab link
                        AddHandler tab.Click, AddressOf tab_Click
                        ' Step 7: Set any other properties as desired
                        tab.Width = _
                           System.Web.UI.WebControls.Unit.Pixel(_tabWidth-2)
tab.Style("text-align") = "center"
                        tab.Style("font-size") = "larger"
                        ' Step 8: Insert tab <A> element into <TD> element
                        tc.Controls.Add(tab)
                        ' Step 9: Insert <TD> element into <TR> element
                        tr.Cells.Add(tc)
                        tabCount += 1
                    End If
                Next
                ' Add final blank cell to cause horizontal line to
                ' run across entire zone width
                tc = New TableCell()
                tc.Text = "&nbsp;"
                tc.Width = _
                   System.Web.UI.WebControls.Unit.Pixel(_tabWidth * 10)
                tc.Style("border-bottom") = "black 1px solid"
                tr.Cells.Add(tc)
                ' Step 10: Insert the <TR> element into <TABLE> and
                ' add the HTML table to the page
                tbl.Rows.Add(tr)
                Me.Controls.Add(tbl)
            Catch ex As Exception
                Dim lbl As New Label()
                lbl.Text = "Error: " + ex.Message
                Me.Controls.Add(lbl)
            End Try
        End Sub
        Protected Overloads Overrides Sub RenderContents( _
           ByVal writer As System.Web.UI.HtmlTextWriter)
            If _debug Then
                writer.Write("Tab Data: " + _tabData + "<hr/>")
            End If
            ShowHideWebParts(writer)
            MyBase.RenderContents(writer)
        End Sub
        ' Show web parts for currently selected tab,
        ' hide all others
        Private Sub ShowHideWebParts( _
           ByVal writer As System.Web.UI.HtmlTextWriter)
            Try
                Dim lbl As New Label()
                Dim arrTabs As String() = _tabData.Split(";"c)
                ' Step 1: If a tab has not been selected, assume
                ' the first one
                If _selectedTab Is Nothing Then
                    _selectedTab = arrTabs(0).Substring(1)
                End If
' Step 2: Hide all web parts in zone that are
                ' below the ZoneTab part
                For Each wp As WebPart In Me.Zone.WebParts
                    If wp.ZoneIndex > Me.ZoneIndex Then
                        wp.Hidden = True
                    End If
                Next
                For i As Integer = 0 To arrTabs.Length - 1
                    ' Step 3: Get web-part names associated with this tab
                    ' Step 4: Find the selected tab
                    If arrTabs(i) = "*" + _selectedTab Then
                        For j As Integer = i + 1 To arrTabs.Length - 1
                            ' Step 5: Get associated web-part names
                            ' Step 6: Loop until next tab name found,
                            ' or end of list
                            If arrTabs(j).IndexOf("*") <> 0 Then
                                ' Step 7: Show named web parts
                                For Each wp As WebPart In Me.Zone.WebParts
                                    If wp.Title = arrTabs(j) Then
                                        wp.Hidden = False
                                    End If
                                Next
                            Else
                                Exit For
                            End If
                        Next
                        ' Step 8: Bring tab border to "front"
                        Dim tc As TableCell = _
                           DirectCast(Me.FindControl("tc_" + _
                           arrTabs(i).Substring(1).Replace(" ", "_")), _
                          TableCell)
                        tc.Style("border-bottom") = "white 1px solid"
                        tc.Style("border-top") = "black 1px solid"
                        tc.Style("border-left") = "black 1px solid"
                        tc.Style("border-right") = "black 1px solid"
                        tc.Style("background-color") = _
                           _tabBackgroundColorSelected
                        ' Step 9: Highlight selected tab
                        Dim tab As LinkButton = _
                           DirectCast(Me.FindControl("tab_" + _
                           arrTabs(i).Substring(1).Replace(" ", "_")), _
                           LinkButton)
                        tab.Style("background-color") = _
                           _tabBackgroundColorSelected
Else
                        If arrTabs(i).IndexOf("*") = 0 Then
                            ' Step 10: Send tab border to "back"
                            Dim tc As TableCell = _
                               DirectCast(Me.FindControl("tc_" + _
                               arrTabs(i).Substring(1).Replace(" ", "_")), _
                               TableCell)
                            tc.Style("border-bottom") = "black 1px solid"
                            tc.Style("border-top") = "gray 1px solid"
                            tc.Style("border-left") = "gray 1px solid"
                            tc.Style("border-right") = "gray 1px solid"
                            tc.Style("background-color") = _
                               _tabBackgroundColorDeselected
                            ' Step 11: Lowlight selected tab
                            Dim tab As LinkButton = _
                               DirectCast(Me.FindControl("tab_" + _
                               arrTabs(i).Substring(1).Replace(" ", "_")), _
                               LinkButton)
                            tab.Style("background-color") = _
                               _tabBackgroundColorDeselected
                        End If
                    End If
                Next
            Catch ex As Exception
                writer.Write("Error: " + ex.Message)
            End Try
        End Sub
        ' This is the click event handler that was assigned
        ' to all tab LinkButton objects in CreateChildControls()
        ' method.
        Private Sub tab_Click(ByVal sender As Object, ByVal e As EventArgs)
            Try
                ' Set flag indicated current tab, for
                ' use in RenderContents() method
                Dim tab As LinkButton = DirectCast(sender, LinkButton)
                _selectedTab = tab.Text
            Catch ex As Exception
                Dim lbl As New Label()
                lbl.Text = ex.Message
                Me.Controls.Add(lbl)
            End Try
        End Sub
    End Class
End Namespace

Recipe—C# (See Project ZoneTabWebPartCS, Class ZoneTabWebPart.cs)

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Drawing;
namespace ZoneTabWebPartCS
{
    public class ZoneTabWebPart : WebPart
    {
        // Local variables
        string _tabData = "";
        bool _debug = false;
        string _selectedTab;
        int _tabWidth = 100;
        string _tabBackgroundColorSelected = "white";
        string _tabBackgroundColorDeselected = "whitesmoke";
        [Personalizable]
        [WebBrowsable]
        [WebDisplayName(
           "Flag indicating whether debug info should be displayed")]
        public bool Debug
        {
            get { return _debug; }
            set { _debug = value; }
        }
        // String containing semicolon-delimited list
        // of tab names.  Tab names are preceeded by "*".
        //
        // Example: *Tab 1;webpart1;webpart2;*Tab 2;webpart3
        //
        [Personalizable]
        [WebBrowsable]
        [WebDisplayName(
           "A delimited list of tab names and associated web parts")]
        public string TabData
        {
            get { return _tabData; }
            set { _tabData = value; }
        }
[Personalizable]
        [WebBrowsable]
        [WebDescription("Color of selected tab")]
        public string SelectedColor
        {
            get { return _tabBackgroundColorSelected; }
            set { _tabBackgroundColorSelected = value; }
        }
        [Personalizable]
        [WebBrowsable]
        [WebDescription("Color of un-selected tab")]
        public string DeSelectedColor
        {
            get { return _tabBackgroundColorDeselected; }
            set { _tabBackgroundColorDeselected = value; }
        }
        [Personalizable]
        [WebBrowsable]
        [WebDisplayName("Width in pixels for each tab")]
        public int TabWidth
        {
            get { return _tabWidth; }
            set { _tabWidth = value; }
        }
        // Add tab links to page
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            try
            {
                string[] arrTabs = _tabData.Split(';'),
                // Build list of tabs in the form
                // of an HTML <TABLE> with <A> tags
                // for each tab
                // Step 1: Define <TABLE> and <TR> HTML elements
                Table tbl = new Table();
                tbl.CellPadding = 0;
                tbl.CellSpacing = 0;
                TableRow tr = new TableRow();
                tr.HorizontalAlign = HorizontalAlign.Left;
                // Step 2: Loop through list of tabs, adding
                //  <TD> and <A> HTML elements for each
                TableCell tc;
                LinkButton tab;
                int tabCount = 0;
for (int i = 0; i < arrTabs.Length; i++)
                {
                    if (arrTabs[i].IndexOf("*") == 0)
                    {
                        // Step 3: Add a blank separator cell
                        tc = new TableCell();
                        tc.Text = "&nbsp;";
                        tc.Width =
                           System.Web.UI.WebControls.Unit.Percentage(1);
                        tc.Style["border-bottom"] = "black 1px solid";
                        tr.Cells.Add(tc);
                        // Step 4: Create a <TD> HTML element to hold the tab
                        tc = new TableCell();
                        tc.ID = "tc_" +
                           arrTabs[i].Substring(1).Replace(" ", "_");
                        tc.Width =
                           System.Web.UI.WebControls.Unit.Pixel(_tabWidth);
                        // Step 5: Create an <A> HTML element to represent
                        // the tab.  Discard first character, which
                        // was a "*"
                        tab = new LinkButton();
                        tab.ID = "tab_" +
                           arrTabs[i].Substring(1).Replace(" ", "_");
                        tab.Text = arrTabs[i].Substring(1);
                        // Step 6: Attach event handler that will execute
                        // when user clicks tab link
                        tab.Click += new EventHandler(tab_Click);
                        // Step 7: Set any other properties as desired
                        tab.Width =
                           System.Web.UI.WebControls.Unit.Pixel(_tabWidth-2);
                        tab.Style["text-align"] = "center";
                        tab.Style["font-size"] = "larger";
                        // Step 8: Insert tab <A> element into <TD> element
                        tc.Controls.Add(tab);
                        // Step 9: Insert <TD> element into <TR> element
                        tr.Cells.Add(tc);
                        tabCount++;
                    }
                }
                // Add final blank cell to cause horizontal line to
                // run across entire zone width
                tc = new TableCell();
                tc.Text = "&nbsp;";
                tc.Width =
                   System.Web.UI.WebControls.Unit.Pixel(_tabWidth * 10);
                tc.Style["border-bottom"] = "black 1px solid";
                tr.Cells.Add(tc);
// Step 10: Insert the <TR> element into <TABLE> and
                // add the HTML table to the page
                tbl.Rows.Add(tr);
                this.Controls.Add(tbl);
            }
            catch (Exception ex)
            {
                Label lbl = new Label();
                lbl.Text = "Error: " + ex.Message;
                this.Controls.Add(lbl);
            }
        }
        protected override void RenderContents(
           System.Web.UI.HtmlTextWriter writer)
        {
            if (_debug)
            {
                writer.Write("Tab Data: " + _tabData + "<hr/>");
            }
            ShowHideWebParts(writer);
            base.RenderContents(writer);
        }
        // Show web parts for currently selected tab,
        // hide all others
        void ShowHideWebParts(System.Web.UI.HtmlTextWriter writer)
        {
            try
            {
                Label lbl = new Label();
                string[] arrTabs = _tabData.Split(';'),
                // Step 1: If a tab has not been selected, assume
                // the first one
                if (_selectedTab == null)
                   _selectedTab = arrTabs[0].Substring(1);
                // Step 2: Hide all web parts in zone that are
                // below the ZoneTab part
                foreach (WebPart wp in this.Zone.WebParts)
                {
                    if (wp.ZoneIndex > this.ZoneIndex)
                    {
                        wp.Hidden = true;
                    }
                }
                // Step 3: Get web-part names associated with this tab
for (int i = 0; i < arrTabs.Length; i++)
                {
                    // Step 4: Find the selected tab
                    if (arrTabs[i] == "*" + _selectedTab)
                    {
                        // Step 5: Get associated web-part names
                        for (int j = i + 1; j < arrTabs.Length; j++)
                        {
                            // Step 6: Loop until next tab name found,
                            // or end of list
                            if (arrTabs[j].IndexOf("*") != 0)
                            {
                                // Step 7: Show named web parts
                                foreach (WebPart wp in this.Zone.WebParts)
                                {
                                    if (wp.Title == arrTabs[j])
                                       wp.Hidden = false;
                                }
                            }
                            else
                            {
                                break;
                            }
                        }
                        // Step 8: Bring tab border to "front"
                        TableCell tc =
                           (TableCell)this.FindControl("tc_" +
                           arrTabs[i].Substring(1).Replace(" ", "_"));
                        tc.Style["border-bottom"] = "white 1px solid";
                        tc.Style["border-top"] = "black 1px solid";
                        tc.Style["border-left"] = "black 1px solid";
                        tc.Style["border-right"] = "black 1px solid";
                        tc.Style["background-color"] =
                           _tabBackgroundColorSelected;
                        // Step 9: Highlight selected tab
                        LinkButton tab =
                           (LinkButton)this.FindControl("tab_" +
                           arrTabs[i].Substring(1).Replace(" ", "_"));
                        tab.Style["background-color"] =
                           _tabBackgroundColorSelected;
                    }
                    else
                    {
                        if (arrTabs[i].IndexOf("*") == 0)
                        {
// Step 10: Send tab border to "back"
                            TableCell tc =
                               (TableCell)this.FindControl("tc_" +
                               arrTabs[i].Substring(1).Replace(" ", "_"));
                            tc.Style["border-bottom"] = "black 1px solid";
                            tc.Style["border-top"] = "gray 1px solid";
                            tc.Style["border-left"] = "gray 1px solid";
                            tc.Style["border-right"] = "gray 1px solid";
                            tc.Style["background-color"] =
                               _tabBackgroundColorDeselected;
                            // Step 11: Lowlight selected tab
                            LinkButton tab =
                               (LinkButton)this.FindControl("tab_" +
                               arrTabs[i].Substring(1).Replace(" ", "_"));
                            tab.Style["background-color"] =
                               _tabBackgroundColorDeselected;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                writer.Write("Error: " + ex.Message);
            }
        }
        // This is the click event handler that was assigned
        // to all tab LinkButton objects in CreateChildControls()
        // method.
        void tab_Click(object sender, EventArgs e)
        {
            try
            {
                // Set flag indicatingcurrent tab, for
                // use in RenderContents() method
                LinkButton tab = (LinkButton)sender;
                _selectedTab = tab.Text;
            }
            catch (Exception ex)
            {
                Label lbl = new Label();
                lbl.Text = ex.Message;
                this.Controls.Add(lbl);
            }
        }
    }
}

To Run

Note

Please see the "To Run" section of Recipe 4-1 for instructions on how to deploy a web part to a single site collection. After the web part has been successfully deployed, proceed with the following steps.

After you have deployed the ZoneTab web part, browse to a web-part page in the site collection to which the ZoneTab has been deployed. Add the ZoneTab to a page and set the properties as shown in Figures 4-16 and 4-17.

Note

Your actual values may vary depending on the web parts you want to show or hide on your page.

The ZoneTab web part's property sheet

Figure 4.16. The ZoneTab web part's property sheet

Syntax for entering tab names and associated web parts for each tab

Figure 4.17. Syntax for entering tab names and associated web parts for each tab

In the preceding screen shots, you can see the ZoneTab web-part property sheet, along with an expanded view of the text that defines the tabs and associated named web parts, which will be displayed when a particular tab is selected. Figure 4-18 shows the ZoneTab web part as it will display on a web-part page.

The ZoneTab web part in action

Figure 4.18. The ZoneTab web part in action

Variations

  • This recipe uses simple outlining to give the impression of tabs. Alternatively, you can set a background image for the <TD> table cells that represent the tabs to give a more realistic impression of tabs.

  • When a page refresh occurs, the first tab will always be reselected. You could save the selected tab to a cookie on the user's computer, and then reset the tab based on the cookie value, to cause the last selected tab to be reselected when the user returns to this page.

Creating a Web Part to Edit SPWeb Properties

One of the better-kept secrets in SharePoint is that each site has a property collection that you can use to store site-specific data, as long as you don't delete or edit the properties that Microsoft stores in that collection. This collection can be a convenient place to store small amounts of data that you want to use to control a web site. For example, you could use it to store the client code of the client for which a particular site was created, and retrieve that property in other web parts or programs to control their behavior. Retrieving a property is simpler than storing the same data in a SharePoint list, so it means less code.

In this recipe, you'll create a little web part to add, edit, or delete site properties that have a specific prefix. Using a settable prefix, we can filter properties so we don't accidentally edit one of the built-in properties used internally by SharePoint.

This recipe takes the use of web-part events a step further than the ZoneTab recipe by attaching a Delete button to each property displayed, wiring those buttons to a common delete event handler, and using data in the sender parameter to determine which property we want to delete.

Because this web part manipulates site information, it must know whether a user is a site administrator to determine whether the list of properties should be read-only or editable. To accomplish that, we'll use the SPWeb.CurrentUser.IsSiteAdmin property.

Tip

If you want this recipe to work for users who have Full Control rights on the site but are not necessarily site collection administrators, use SPWeb.UserIsWebAdmin.

Recipe Type: Web Part

Ingredients

Assembly References

  • System.Web .NET assembly

  • Windows.SharePoint.Services .NET assembly

Special Considerations

  • Unlike several earlier web-part recipes, we'll need the Microsoft.SharePoint namespaces because we're retrieving and manipulating data about a SharePoint web site.

  • SharePoint creates a number of properties for every site that should not be changed or deleted, so this recipe uses a prefix for every property created. The prefix default is mg_ but you can change it to any value you want, as long as you don't use vti_ (which is the prefix used by Microsoft).

Preparation

  1. Create a new C# or VB.NET class library.

  2. Add references to the System.Web and Windows.SharePoint.Services .NET assemblies.

  3. On the project properties Signing tab, select the Sign the Assembly checkbox and specify a new strong-name key file.

  4. Add public properties as shown in the following source code.

  5. Override the CreateChildControls() base web-part method as shown in the following code.

  6. Add the btnClick() and delbtnClick() event handlers.

  7. Override the RenderContents() method to call the ShowHideWebParts() method.

Process Flow: CreateChildControls()

Process Flow: CreateChildControls()

The CreateChildControls() method is fairly interesting in that in it we build a dynamic <TABLE> to contain the list of properties in the site matching our prefix, and for site administrators, Add, Delete, and Save buttons as well.

  1. Create an in-memory <TABLE> object to hold the list of matching properties.

  2. Loop through the properties for this web site, finding those whose key begins with the prefix specified in the web part prefix property.

  3. If the user is a site administrator, place the property value in a text box, and include a Delete button to the right of the text box. Otherwise, just write the literal text of the property value to the page.

  4. When finished writing all matching properties, determine whether the user is a site administrator.

  5. If the user is the site administrator, add a blank row in which the user can add a new property.

  6. Add a button to enable the user to save changes to existing or new properties.

Process Flow: btn_Click()

Process Flow: btn_Click()

The btn_Click() event handler fires when the user clicks the Save Changes button, and is responsible for writing changed or new properties back to SharePoint.

  1. Create an SPWeb object from the current context that points to the web site in which the current web-part page exists.

  2. If the user entered a new property in the blank row at the bottom of the form, add a new property to the site with the same key name and value.

  3. Loop through all text boxes in the web part that have an ID of key_<n>, where <n> is between 1 and 999.

  4. Continue looping until no more text boxes with that ID pattern are found.

  5. Determine whether the text box found has a text value that is different from the corresponding site property.

  6. If the text box does have a different value, update the site property with the text box's value.

Process Flow: delbtn_Click()

Process Flow: delbtn_Click()

The delbtn_Click() event handler fires when the user clicks a Delete button to the right of an editable property, and is responsible for removing the corresponding site property from the property collection.

  1. Find the name of the property to delete by inspecting the object (in this case, the specific Delete button) that fired the event.

  2. Create an SPWeb object pointing to the current site.

  3. Set the value of the property to be deleted to null, which will cause it to be removed from the collection. Note that there is a Remove() method on the Properties collection, but it failed to delete the property, so I chose this method instead.

  4. Call the CreateChildControls() method again to redraw the <TABLE> of matching properties without the deleted property.

  5. Display a message below the table informing the end user that the property has been deleted.

Recipe—VB (See Project WebPropertiesWebPartVB, Class WebPropertiesWebPart.vb)

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Web.UI.WebControls
Imports System.Web.UI.WebControls.WebParts
Imports Microsoft.SharePoint
Imports Microsoft.SharePoint.WebControls
Imports Microsoft.SharePoint.Utilities
Public Class WebPropertiesWebPart
    Inherits WebPart
    ' Define location variables
    Private _debug As Boolean = False
    Private _prefix As String = "mg_"
    <Personalizable()> _
    <WebBrowsable()> _
    <WebDescription("Check to display debug information")> _
    <WebDisplayName("Debug?")> _
    Public Property Debug() As Boolean
        Get
            Return _debug
        End Get
        Set(ByVal value As Boolean)
            _debug = value
        End Set
    End Property
<Personalizable()> _
    <WebBrowsable()> _
    <WebDescription("Prefix to use when storing & retrieving properties")> _
    <WebDisplayName("Property prefix: ")> _
    Public Property Prefix() As String
        Get
            Return _prefix
        End Get
        Set(ByVal value As String)
            _prefix = value
        End Set
    End Property
    Protected Overloads Overrides Sub CreateChildControls()
        MyBase.CreateChildControls()
        Try
            ' Step 1: Create table to hold existing, new properties
            Dim tbl As New Table()
            Dim tr As TableRow
            Dim tc As TableCell
            Dim delBtn As ImageButton
            Dim tb As TextBox
            Dim lbl As Label
            Dim i As Integer = 1
            ' Just a bit of formatting
            tbl.CellPadding = 3
            tbl.CellSpacing = 3
            tbl.BorderStyle = BorderStyle.Solid
            ' Add a heading
            tr = New TableRow()
            ' #
            tc = New TableCell()
            tr.Cells.Add(tc)
            ' Key
            tc = New TableCell()
            tc.Text = "Property Key"
            tc.Font.Bold = True
            tr.Cells.Add(tc)
            ' Value
            tc = New TableCell()
            tc.Text = "Value"
            tc.Font.Bold = True
            tr.Cells.Add(tc)
            tbl.Rows.Add(tr)
            ' Delete button
            tc = New TableCell()
            tr.Cells.Add(tc)
            tc.Font.Bold = True
tr.Cells.Add(tc)
            tbl.Rows.Add(tr)
            ' Step 2: Loop through existing properties that match prefix
            ' and are not null, add to table
            Dim web As SPWeb = SPControl.GetContextWeb(Context)
            Dim properties As SPPropertyBag = web.Properties
            Dim isAdmin As Boolean = web.CurrentUser.IsSiteAdmin
            For Each key As Object In properties.Keys
                If key.ToString().IndexOf(_prefix) = 0 _
                   AndAlso properties(key.ToString()) IsNot Nothing Then
                    ' Create a new row for current property
                    tr = New TableRow()
                    ' #
                    tc = New TableCell()
                    tc.Text = i.ToString() + ". "
                    tr.Cells.Add(tc)
                    ' Key
                    tc = New TableCell()
                    tc.Text = key.ToString().Substring(_prefix.Length)
                    tc.ID = "key_" + i.ToString()
                    tr.Cells.Add(tc)
                    ' Value
                    tc = New TableCell()
                    ' 3. For admin users, show value in
                    ' an editable text box + delete button
                    If isAdmin Then
                        tb = New TextBox()
                        tb.Text = properties(key.ToString())
                        tb.ID = "value_" + i.ToString()
                        tc.Controls.Add(tb)
                        tr.Cells.Add(tc)
                        tc = New TableCell()
                        delBtn = New ImageButton()
                        delBtn.ImageUrl = "/_layouts/images/delete.gif"
                        AddHandler delBtn.Click, AddressOf delBtn_Click
                        delBtn.ID = "delete_" + i.ToString()
                        tc.Controls.Add(delBtn)
                        tr.Cells.Add(tc)
                    Else
                        ' for non-admin users, just show read-only
                        lbl = New Label()
                        lbl.Text = properties(key.ToString())
                        tc.Controls.Add(lbl)
                        tr.Cells.Add(tc)
                    End If
' Add new row to table
                    tbl.Rows.Add(tr)
                    i += 1
                End If
            Next
            ' Step 4: Add a final row to allow user
            ' to add new properties if current user is site admin
            If isAdmin Then
                tr = New TableRow()
                ' #
                tc = New TableCell()
                tc.Text = "*. "
                tr.Cells.Add(tc)
                ' Key
                tc = New TableCell()
                tb = New TextBox()
                tb.Text = ""
                tb.ID = "key_new"
                tc.Controls.Add(tb)
                tr.Cells.Add(tc)
                ' Value
                tc = New TableCell()
                tb = New TextBox()
                tb.Text = ""
                tb.ID = "value_new"
                tc.Controls.Add(tb)
                tr.Cells.Add(tc)
                tbl.Rows.Add(tr)
            End If
            ' Step 5: Add the completed table to the page
            Me.Controls.Add(tbl)
            ' Step 6: Now add a button to save changes,
            ' if current user is site admin
            If isAdmin Then
                lbl = New Label()
                lbl.Text = "<br/>"
                Me.Controls.Add(lbl)
                Dim btn As New Button()
                btn.Text = "Save changes"
                AddHandler btn.Click, AddressOf btn_Click
                Me.Controls.Add(btn)
            End If
        Catch ex As Exception
            Dim lbl As New Label()
            lbl.Text = "Error: " + ex.Message
            Me.Controls.Add(lbl)
        End Try
    End Sub
' Handles "Save Changes" button click event
    Private Sub btn_Click(ByVal sender As Object, ByVal e As EventArgs)
        Try
            ' Step 1: Get handle to web site property
            ' collection
            Dim isChanged As Boolean = False
            Dim web As SPWeb = SPControl.GetContextWeb(Context)
            Dim properties As SPPropertyBag = web.Properties
            web.AllowUnsafeUpdates = True
            ' Step 2: Add new property
            Dim tbNewKey As TextBox = _
               DirectCast(Me.FindControl("key_new"), TextBox)
            Dim tbNewValue As TextBox = _
               DirectCast(Me.FindControl("value_new"), TextBox)
            If tbNewKey.Text <> "" Then
                properties(_prefix + tbNewKey.Text) = tbNewValue.Text
                web.Properties.Update()
                isChanged = True
            End If
            ' Step 3: Loop through text boxes in web part,
            ' updating corresponding site property if
            ' checkbox has been changed.
            Dim tc As TableCell
            Dim tb As TextBox
            For i As Integer = 1 To 998
                tc = DirectCast( _
                   Me.FindControl("key_" + i.ToString()), TableCell)
                ' Step 4: If a control with the name "key_<n>" exists, get
                ' it, otherwise assume no more custom properties to edit
                If tc IsNot Nothing Then
                    ' Step 5: Ok, we found the text box containing the
                    ' property value, now let's see if the value in the
                    ' text box has been changed to something other than that
                    ' in the corresponding web property.
                    tb = DirectCast( _
                       Me.FindControl("value_" + i.ToString()), TextBox)
                    If properties(_prefix + tc.Text).Trim() _
                       <> tb.Text.Trim() Then
                        ' Step 6: The value was changed, update the web
                        ' property and set the flag indicating that web part
                        ' needs to be redrawn
                        properties(_prefix + tc.Text) = tb.Text
                        web.Properties.Update()
                        isChanged = True
                    End If
                Else
                    Exit For
                End If
Next
            ' Step 7: If any changes made, redraw web part
            ' to reflect changed/added properties
            If isChanged Then
                Me.Controls.Clear()
                CreateChildControls()
            End If
        Catch ex As Exception
            Dim lbl As New Label()
            lbl.Text = "<br/><br/>Error: " + ex.Message
            Me.Controls.Add(lbl)
        End Try
    End Sub
    ' Handles individual property delete button click
    Private Sub delBtn_Click(ByVal sender As Object, _
       ByVal e As System.Web.UI.ImageClickEventArgs)
        Try
            ' Step 1. Get handle to name of property to be
            ' deleted
            Dim delBtn As ImageButton = DirectCast(sender, ImageButton)
            Dim _id As String = delBtn.ID.Replace("delete_", "")
            Dim tc As TableCell = _
               DirectCast(Me.FindControl("key_" + _id), TableCell)
            ' Step 2: Get handle to web site, property collection
            Dim web As SPWeb = SPControl.GetContextWeb(Context)
            Dim properties As SPPropertyBag = web.Properties
            web.AllowUnsafeUpdates = True
            ' Step 3: Delete the unwanted property by setting
            ' its value to null (note: for some reason using
            ' the Remove() method was not sufficient to cause
            ' SharePoint to delete the property).
            web.Properties(_prefix + tc.Text) = Nothing
            web.Properties.Update()
            web.Update()
            ' Step 4: Refresh list
            Me.Controls.Clear()
            CreateChildControls()
            ' Step 5: Display message to user informing them
            ' that property has been deleted
            Dim lbl As New Label()
            lbl.Text = "<br/><br/>You deleted property '" + tc.Text + "'"
            Me.Controls.Add(lbl)
        Catch ex As Exception
            Dim lbl As New Label()
            lbl.Text = "<br/><br/>Error: " + ex.Message
            Me.Controls.Add(lbl)
        End Try
End Sub
    Protected Overloads Overrides Sub RenderContents( _
       ByVal writer As System.Web.UI.HtmlTextWriter)
        MyBase.RenderContents(writer)
        If _debug Then
            writer.Write("<hr/>")
            writer.Write("<strong>Prefix:</strong> " + _prefix)
            writer.Write("<hr/>")
        End If
    End Sub
End Class

Recipe—C# (See Project WebPartPropertiesCS, Class WebPartProperties.cs)

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.Utilities;
namespace WebPropertiesWebPartCS
{
    public class WebPropertiesWebPart : WebPart
    {
        // Define location variables
        bool _debug = false;
        string _prefix = "mg_";
        [Personalizable]
        [WebBrowsable]
        [WebDescription("Check to display debug information")]
        [WebDisplayName("Debug?")]
        public bool Debug
        {
            get { return _debug; }
            set { _debug = value; }
        }
        [Personalizable]
        [WebBrowsable]
        [WebDescription(
           "Prefix to use when storing and retrieving properties")]
        [WebDisplayName("Property prefix: ")]
        public string Prefix
        {
            get { return _prefix; }
            set { _prefix = value; }
        }
protected override void CreateChildControls()
        {
            base.CreateChildControls();
            try
            {
                // Step 1: Create table to hold existing, new properties
                Table tbl = new Table();
                TableRow tr;
                TableCell tc;
                ImageButton delBtn;
                TextBox tb;
                Label lbl;
                int i = 1;
                // Just a bit of formatting
                tbl.CellPadding = 3;
                tbl.CellSpacing = 3;
                tbl.BorderStyle = BorderStyle.Solid;
                // Add a heading
                tr = new TableRow();
                // #
                tc = new TableCell();
                tr.Cells.Add(tc);
                // Key
                tc = new TableCell();
                tc.Text = "Property Key";
                tc.Font.Bold = true;
                tr.Cells.Add(tc);
                // Value
                tc = new TableCell();
                tc.Text = "Value";
                tc.Font.Bold = true;
                tr.Cells.Add(tc);
                tbl.Rows.Add(tr);
                // Delete button
                tc = new TableCell();
                tr.Cells.Add(tc);
                tc.Font.Bold = true;
                tr.Cells.Add(tc);
                tbl.Rows.Add(tr);
                // Step 2: Loop through existing properties that match prefix
                // and are not null, add to table
                SPWeb web = SPControl.GetContextWeb(Context);
                SPPropertyBag properties = web.Properties;
                bool isAdmin = web.CurrentUser.IsSiteAdmin;
foreach (object key in properties.Keys)
                {
                    if (key.ToString().IndexOf(_prefix) == 0
                       && properties[key.ToString()] != null)
                    {
                        // Create a new row for current property
                        tr = new TableRow();
                        // #
                        tc = new TableCell();
                        tc.Text = i.ToString() + ". ";
                        tr.Cells.Add(tc);
                        // Key
                        tc = new TableCell();
                        tc.Text = key.ToString().Substring(_prefix.Length);
                        tc.ID = "key_" + i.ToString();
                        tr.Cells.Add(tc);
                        // Value
                        tc = new TableCell();
                        // 3. For admin users, show value in
                        // an editable text box + delete button
                        if (isAdmin)
                        {
                            tb = new TextBox();
                            tb.Text = properties[key.ToString()];
                            tb.ID = "value_" + i.ToString();
                            tc.Controls.Add(tb);
                            tr.Cells.Add(tc);
                            tc = new TableCell();
                            delBtn = new ImageButton();
                            delBtn.ImageUrl = "/_layouts/images/delete.gif";
                            delBtn.Click += new
                               System.Web.UI.ImageClickEventHandler(delBtn_Click);
                            delBtn.ID = "delete_" + i.ToString();
                            tc.Controls.Add(delBtn);
                            tr.Cells.Add(tc);
                        }
                        else // for non-admin users, just show read-only
                        {
                            lbl = new Label();
                            lbl.Text = properties[key.ToString()];
                            tc.Controls.Add(lbl);
                            tr.Cells.Add(tc);
                        }
                        // Add new row to table
                        tbl.Rows.Add(tr);
                        i++;
                    }
                }
// Step 4: Add a final row to allow user
                // to add new properties if current user is site admin
                if (isAdmin)
                {
                    tr = new TableRow();
                    // #
                    tc = new TableCell();
                    tc.Text = "*. ";
                    tr.Cells.Add(tc);
                    // Key
                    tc = new TableCell();
                    tb = new TextBox();
                    tb.Text = "";
                    tb.ID = "key_new";
                    tc.Controls.Add(tb);
                    tr.Cells.Add(tc);
                    // Value
                    tc = new TableCell();
                    tb = new TextBox();
                    tb.Text = "";
                    tb.ID = "value_new";
                    tc.Controls.Add(tb);
                    tr.Cells.Add(tc);
                    tbl.Rows.Add(tr);
                }
                // Step 5: Add the completed table to the page
                this.Controls.Add(tbl);
                // Step 6: Now add a button to save changes,
                // if current user is site admin
                if (isAdmin)
                {
                    lbl = new Label();
                    lbl.Text = "<br/>";
                    this.Controls.Add(lbl);
                    Button btn = new Button();
                    btn.Text = "Save changes";
                    btn.Click += new EventHandler(btn_Click);
                    this.Controls.Add(btn);
                }
            }
            catch (Exception ex)
            {
                Label lbl = new Label();
                lbl.Text = "Error: " + ex.Message;
                this.Controls.Add(lbl);
            }
        }
// Handles "Save Changes" button click event
        void btn_Click(object sender, EventArgs e)
        {
            try
            {
                // Step 1: Get handle to web site property
                // collection
                bool isChanged = false;
                SPWeb web = SPControl.GetContextWeb(Context);
                SPPropertyBag properties = web.Properties;
                web.AllowUnsafeUpdates = true;
                // Step 2: Add new property
                TextBox tbNewKey = (TextBox)this.FindControl("key_new");
                TextBox tbNewValue = (TextBox)this.FindControl("value_new");
                if (tbNewKey.Text != "")
                {
                    properties[_prefix+tbNewKey.Text] = tbNewValue.Text;
                    web.Properties.Update();
                    isChanged = true;
                }
                // Step 3: Loop through text boxes in web part
                // updating corresponding site property if
                // checkbox has been changed.
                TableCell tc;
                TextBox tb;
                for (int i = 1; i < 999; i++)
                {
                    tc = (TableCell)this.FindControl("key_"+i.ToString());
                    // Step 4: If a control with the name "key_<n>"
                    // exists, get it, otherwise assume no more custom
                    // properties to edit
                    if (tc != null)
                    {
                        // Step 5: Ok, we found the text box containing the
                        // property value, now let's see if the value in the
                        // text box has been changed to something other than
                        // that in the corresponding web property.
                        tb = (TextBox)this.FindControl("value_" +
                           i.ToString());
                        if (properties[_prefix+tc.Text].Trim() !=
                           tb.Text.Trim())
                        {
                            // Step 6: The value was changed, update the web
                            // property and set the flag indicating that web
                            // part needs to be redrawn
                            properties[_prefix+tc.Text] = tb.Text;
web.Properties.Update();
                            isChanged = true;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
                // Step 7: If any changes made, redraw web part
                // to reflect changed/added properties
                if (isChanged)
                {
                    this.Controls.Clear();
                    CreateChildControls();
                }
            }
            catch (Exception ex)
            {
                Label lbl = new Label();
                lbl.Text = "<br/><br/>Error: " + ex.Message;
                this.Controls.Add(lbl);
            }
        }
        // Handles individual property delete button click
        void delBtn_Click(object sender, System.Web.UI.ImageClickEventArgs e)
        {
            try
            {
                // Step 1. Get handle to name of property to be
                // deleted
                ImageButton delBtn = (ImageButton)sender;
                string _id = delBtn.ID.Replace("delete_", "");
                TableCell tc = (TableCell)this.FindControl("key_" + _id);
                // Step 2: Get handle to web site, property collection
                SPWeb web = SPControl.GetContextWeb(Context);
                SPPropertyBag properties = web.Properties;
                web.AllowUnsafeUpdates = true;
                // Step 3: Delete the unwanted property by setting
                // its value to null (note: for some reason using
                // the Remove() method was not sufficient to cause
                // SharePoint to delete the property).
                web.Properties[_prefix + tc.Text] = null;
                web.Properties.Update();
                web.Update();
// Step 4: Refresh list
                this.Controls.Clear();
                CreateChildControls();
                // Step 5: Display message to user informing them
                // that property has been deleted
                Label lbl = new Label();
                lbl.Text = "<br/><br/>You deleted property '" +
                   tc.Text + "'";
                this.Controls.Add(lbl);
            }
            catch (Exception ex)
            {
                Label lbl = new Label();
                lbl.Text = "<br/><br/>Error: " + ex.Message;
                this.Controls.Add(lbl);
            }
        }
        protected override void RenderContents(
           System.Web.UI.HtmlTextWriter writer)
        {
            base.RenderContents(writer);
            if (_debug)
            {
                writer.Write("<hr/>");
                writer.Write("<strong>Prefix:</strong> " + _prefix);
                writer.Write("<hr/>");
            }
        }
    }
}

To Run

Note

Please see the "To Run" section of Recipe 4-1 for instructions on how to deploy a web part to a single site collection. After the web part has been successfully deployed, proceed with the following steps.

Open a web-part page in the site collection to which you have deployed the Web Properties web part, edit the page, and add an instance of the web part. Edit the web part properties to set the prefix you wish to use for properties that will be added or edited through this web part.

Figure 4-19 shows a Web Properties web part as it will be displayed for a user who is a site collection administrator, on a site where five properties have already been added: client#, client name, status, matter name, and #. A new property with a key of new key and value of new value will be added to the property collection when the Save Changes button is clicked. Figure 4-19 shows the Web Properties web part with several custom web site properties already added.

The Web Properties web part shown with custom web properties

Figure 4.19. The Web Properties web part shown with custom web properties

Variations

  • Although this recipe is designed to edit the current site's property collection, the general pattern can be adapted to any collection of key/value pairs. This could be items from a SharePoint list or even from an external database—with minor changes, the event handling will be the same.

  • The recipe does not currently prompt the user to confirm a deletion. It would be fairly simple to add some client-side JavaScript to confirm that the user wants to delete a selected property and cancel the event if requested.

  • Make a connectable web part that reads from the property bag (for instance, store the primary key of the web's associated entity) and then provides the value to multiple other web parts on the page that then use that value to read information from a database or filter their data.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.96.102