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!
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).
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.
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.
Create a new C# or Visual Basic .NET (VB.NET) class library.
Add references to the System.Web
and Windows.SharePoint.Services
.NET assemblies.
On the project properties Signing tab, select the Sign the Assembly checkbox and specify a new strong-name key file.
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.
Read the RSS XML into an ADO DataSet
object.
If an error occurred while reading the RSS source, display the resulting error to the web-part page.
Loop through each article
returned in the RSS XML.
Write the title, description, and link to the page.
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
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; ); } } } }
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.
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
Figure 4-2 shows the RSS web part in action.
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.
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.
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.
Create a new C# or VB.NET class library project.
Add a reference to the System.Web
.NET assembly.
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.
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
.
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.
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.
If the user did not supply a URL to load, display a message indicating that it's required and exit.
Create a new WebRequest
object.
If the user has selected the Impersonate
web-part property, set the request credentials to be those of the current user.
Otherwise, set the credentials based on the explicitly provided domain, user, and password.
Read the XML document into a DataSet
object.
If the user has specified that the data should be displayed by using a DataGrid
, go to step 9. Otherwise, go to step 10.
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.
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.
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
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>"); } } } }
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>
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>
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.
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.
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.
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.
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.
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.
Create a new C# or VB.NET class library project.
Add a reference to the System.Web
.NET assembly.
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.
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
.
If the web part's debug property has been selected, display the property settings prior to displaying the formatted output.
Query the database to return one or more tables in a result set.
Load the result set into a DataSet
object.
Determine whether the format
property has been set to DataGrid
or XSLT.
If it has been set to DataGrid
, assign the DataSet
as the source for a DataGrid
web control and display that.
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.
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
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); } } } }
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"> <tr> </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"> </tr> </xsl:text> </xsl:if> </xsl:for-each> </table> </xsl:template> </xsl:stylesheet>
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.
After you have added the SQL web part to a web-part page, you're ready to try it out.
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 |
|
Table name |
|
User login |
|
Connection string |
|
SQL query |
|
Table definition |
|
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.
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.
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.
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).
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.
Add a reference to the System.Web
.NET assembly.
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.
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
.
If the Debug property has been selected on the web-part properties page, write all properties to the web-part page.
If the URL property was not provided, there's nothing more to do. Warn the user that a URL is required and stop processing.
Create a .NET web request object to read the specified URL into memory.
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.
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.
Display the contents of the URL read.
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
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); } } } }
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.
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
.
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%'> </IFRAME>");
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.
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 include
s) 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.
Add a reference to the System.Web
.NET assembly.
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.
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
.
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.
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.
Create a provider web-part class. Note that this class will also implement the interface created in step 1.
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.
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.
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.
Create a consumer web-part class.
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.
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.
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
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 } } } }
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.
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
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.
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.
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.
Create a custom list called Clients
as described in the "To Run" section.
Create a new C# or VB.NET class library.
Add references to the Windows.SharePoint.Services
and System.Web
.NET assemblies.
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
Add the properties specified in the following source code.
Override the RenderContents()
base web part method.
Add the custom displayClientData()
method.
Create an XML transform (XSLT) to format the resulting data.
If the Debug checkbox is selected, loop through each querystring parameter and write to the web-part page.
Write values of web-part properties that determine output format and (if format
is XSLT
) the XSLT transform file to use.
Instantiate an SPWeb
object representing the web site that the current web-part page is a member of.
Get a handle to the Clients
list—assuming one exists.
Use the SPListItems.GetDataTable()
method to write the entire contents of the list into an ADO.NET DataTable
for easier processing.
Use an ADO.NET DataView
object to filter the DataTable
created in step 4 based on the client ID passed in the querystring.
Determine whether the web part has been set to format data by using a DataGrid or XSLT.
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.
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.
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
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>"); } } } }
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>
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.
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.
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 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.
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.
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.
Download and install the SmartPart, which as of this writing is available at www.codeplex.com/smartpart/Release/ProjectReleases.aspx?ReleaseId=10697
.
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
.
Create a new C# or VB.NET ASP.NET web application project.
Add a user control to the project and name it something like SmartPartStatusWebPart.ascx
.
Add a reference to the Windows.SharePoint.Services
.NET assembly.
Add using
or Includes
statements for the Microsoft.SharePoint
and Microsoft.SharePoint.WebControls
namespaces.
Copy the three icon files to the SharePoint images folder, which is typically at C:Program FilesCommon FilesMicrosoft Sharedweb server extensions12TEMPLATEIMAGES
.
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.
Instantiate SPSite
, SPWeb
, and SPList
objects to obtain a handle to the list.
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.
If a match is found, return the associated value of the Status
column and exit.
Otherwise, if no match is found, return an empty string.
Instantiate SPSite
, SPWeb
, and SPList
objects to obtain a handle to the list.
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.
If a match is found, update the Status
field with the color value of the currently selected radio button.
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.
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.
All of the preceding radio buttons need to be given the same GroupName
property, and their AutoPostBack
property needs to be set to true
.
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
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(); } }
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.
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!
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.
Create a new C# or VB.NET class library.
Add references to the System.Web
and System.Drawing
.NET assemblies.
On the project properties Signing tab, select the Sign the Assembly checkbox and specify a new strong-name key file.
Add public properties as shown in the following source code.
Override the CreateChildControls()
base web-part method.
Create an event handler for the tab Click
events.
Create the ShowHideWebParts()
custom method as shown in the following source code.
Override the RenderContents()
method to call the ShowHideWebParts()
method.
The first of the two most interesting processes in this recipe is the overridden CreateChildControls()
method that is responsible for drawing the tab menu.
Create an in-memory representation of an HTML <TABLE>
element, and a single <TR>
element that will contain our tabs.
Loop through the list of tabs and their associated named web parts. Tabs are designated by a leading asterisk (*
) in the _tabData
variable.
Add a space between tabs.
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.
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.
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.
Finish setting properties that will affect how each tab displays.
Add the <A>
hyperlink object to the <TD>
table cell object.
Add the <TD>
cell to the <TR>
table row object.
When all tabs have been added to the <TR>
object, add that to the <TABLE>
object and render the entire <TABLE>
to the page.
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. |
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 = " " 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 = " " 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
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 = " "; 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 = " "; 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); } } } }
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.
Your actual values may vary depending on the web parts you want to show or hide on your page.
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.
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.
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.
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
.
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).
Create a new C# or VB.NET class library.
Add references to the System.Web
and Windows.SharePoint.Services
.NET assemblies.
On the project properties Signing tab, select the Sign the Assembly checkbox and specify a new strong-name key file.
Add public properties as shown in the following source code.
Override the CreateChildControls()
base web-part method as shown in the following code.
Add the btnClick()
and delbtnClick()
event handlers.
Override the RenderContents()
method to call the ShowHideWebParts()
method.
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.
Create an in-memory <TABLE>
object to hold the list of matching properties.
Loop through the properties for this web site, finding those whose key begins with the prefix specified in the web part prefix
property.
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.
When finished writing all matching properties, determine whether the user is a site administrator.
If the user is the site administrator, add a blank row in which the user can add a new property.
Add a button to enable the user to save changes to existing or new properties.
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.
Create an SPWeb
object from the current context that points to the web site in which the current web-part page exists.
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.
Loop through all text boxes in the web part that have an ID of key_<n>
, where <n>
is between 1 and 999.
Continue looping until no more text boxes with that ID pattern are found.
Determine whether the text box found has a text value that is different from the corresponding site property.
If the text box does have a different value, update the site property with the text box's value.
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.
Find the name of the property to delete by inspecting the object (in this case, the specific Delete button) that fired the event.
Create an SPWeb
object pointing to the current site.
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.
Call the CreateChildControls()
method again to redraw the <TABLE>
of matching properties without the deleted property.
Display a message below the table informing the end user that the property has been deleted.
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
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/>"); } } } }
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.
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.
3.145.96.102