Defining SharePoint Content Types in Code
There are many advantages to using the Windows SharePoint Services 3.0 object model instead of XML to define content types. These advantages include:
- No need to refer to the GUIDs of built-in site columns
- The ability to create dynamic types that depend on runtime conditions
- The ability to build a library of reusable content type components
Automatic Resolution of Built-In Field Identifiers
Setting up field references for content types declared using XML requires that the unique field identifier is known ahead of time. When creating field references in code, you only need to supply the associated field name. SharePoint retrieves the identifier automatically.
For example, the following code segment creates a Project Proposal content type based on the built-in Document type, and then adds an Author column to the new content type. The Author column is provided by SharePoint as one of the built-in site columns available in the Document Columns group.
using (SPSite site = new SPSite("http://localhost")) {
using (SPWeb web = site.OpenWeb()) {
SPContentType baseType = web.AvailableContentTypes["Document"];
SPContentType proposal = new SPContentType(
baseType, web.ContentTypes, "Project Proposal");
web.ContentTypes.Add(proposal);
proposal.FieldLinks.Add(new SPFieldLink(web.AvailableFields["Author"]));
}
}
Dynamic Content Type Definitions
With XML content type definitions, the fields are declared statically at design time. Once the content type is deployed and provisioned, its fields cannot be changed without rewriting the solution. On the other hand, by using the object model, you can setup the content type differently depending on external conditions. This way you can build smarter solutions that adjust automatically to accommodate changes in the runtime environment.
Building a Library of Reusable Content Type Components
When working with the Windows SharePoint Services 3.0 object model, it is useful to create a set of helper components to simplify solution development. This can greatly reduce the steps needed to build a solution because the low-level details of working with the object model are tucked away inside higher level abstractions that are easier to declare and use. This is especially important when building document management solutions based on content types because you ultimately want to encapsulate the business rules within the content type itself. Having a library of core components means you don't have to start from scratch each time you need a new content type.
Listing 1 shows a generic ContentType class that is used as a wrapper for the underlying SPContentType object instance.
Listing 1: A Generic Content Type Wrapper Class
using System;
using Microsoft.SharePoint;
namespace ProSharePoint2007
{
/// <summary>
/// A utility class for manipulating SharePoint content types.
/// </summary>
public class ContentType
{
SPContentType m_contentType = null;
/// <summary>
/// Default constructor.
/// </summary>
public ContentType()
{
}
/// <summary>
/// Creates a wrapper for an existing content type instance.
/// </summary>
/// <param name="contentType"></param>
public ContentType(SPContentType contentType)
{
m_contentType = contentType;
}
/// <summary>
/// Adds a content type to a SharePoint list.
/// </summary>
public static void AddToList(SPList list, SPContentType contentType)
{
list.ContentTypesEnabled = true;
list.ContentTypes.Add(contentType);
list.Update();
}
/// <summary>
/// Removes a content type from a SharePoint list.
/// </summary>
public static void RemoveFromList(SPList list, string contentTypeName)
{
foreach (SPContentType type in list.ContentTypes) {
if (type.Name == contentTypeName) {
list.ContentTypes.Delete(type.Id);
list.Update();
break;
}
}
}
/// <summary>
/// Loads a pre-existing content type.
/// </summary>
public virtual SPContentType Create(SPWeb web, string typeName)
{
try {
m_contentType = web.AvailableContentTypes[typeName];
} catch {
}
return m_contentType;
}
/// <summary>
/// Creates a new content type.
/// </summary>
public virtual SPContentType Create(SPWeb web, string typeName,
string baseTypeName,
string description)
{
try {
SPContentType baseType = (baseTypeName == null
|| baseTypeName.Length == 0) ?
web.AvailableContentTypes[SPContentTypeId.Empty] :
web.AvailableContentTypes[baseTypeName];
m_contentType = new SPContentType(
baseType, web.ContentTypes, typeName);
m_contentType.Description = description;
web.ContentTypes.Add(m_contentType);
} catch {
}
return m_contentType;
}
/// <summary>
/// Conversion operator to access the underlying SPContentType instance.
/// </summary>
public static implicit operator SPContentType(ContentType t){
return t.m_contentType;
}
#region Field Methods
/// <summary>
/// Adds a new field having a specified name and type.
/// </summary>
public SPField AddField(string fieldDisplayName,
SPFieldType fieldType, bool bRequired)
{
SPField field = null;
try {
// get the parent web
using (SPWeb web = m_contentType.ParentWeb) {
// create the field within the target web
string fieldName =
web.Fields.Add(fieldDisplayName,
fieldType, bRequired);
field = web.Fields[fieldName];
// add a field link to the content type
m_contentType.FieldLinks.Add(
new SPFieldLink(field));
m_contentType.Update(false);
}
} catch {
}
return field;
}
/// <summary>
/// Adds a new field based on an existing field in the parent web.
/// </summary>
public SPField AddField(string fieldName)
{
using (SPWeb web = m_contentType.ParentWeb) {
SPField field = web.AvailableFields[fieldName];
try {
if (field != null) {
m_contentType.FieldLinks.Add(
new SPFieldLink(field));
m_contentType.Update(false);
}
} catch {
}
}
return field;
}
#endregion
}
}
With this helper class in the component library, it's easy to declare a new project proposal content type. It can be instantiated either from an XML definition associated with a feature, or it can be created entirely in code. Listing 2 shows the declaration for the project proposal type derived from our generic content type wrapper class.
Listing 2: Using the Content Type Wrapper Class
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
namespace ProSharePoint2007
{
/// <summary>
/// A helper class that encapsulates the ProjectProposal content type.
/// </summary>
class ProjectProposalType : ContentType
{
/// <summary>
/// Creates the type using the XML content type definition.
/// </summary>
public SPContentType Create(SPWeb web)
{
return this.Create(web, "Project Proposal");
}
/// <summary>
/// Creates the type using the SharePoint object model.
/// </summary>
public override SPContentType Create(SPWeb web, string typeName,
string baseTypeName,
string description)
{
// Call the base method to create the new type.
SPContentType tProposal = base.Create(web, typeName,
baseTypeName, description);
// Create the fields programmatically.
if (tProposal != null) {
// built-in fields
AddField("Author");
AddField("Subject");
AddField("StartDate");
AddField("EndDate");
AddField("Status");
AddField("Comments");
AddField("Keywords");
// custom fields
AddField(Strings._Field_ProposalType);
AddField(Strings._Field_EstimatedCost);
AddField(Strings._Field_BidAmount);
AddField(Strings._Field_EstimatedHours);
AddField(Strings._Field_HourlyRate);
}
return tProposal;
}
}
}
This code produces the content type definition shown in Figure 3.
Figure 3
In order to use the content type in a SharePoint site, you must deploy the type definition and then attach it to a list or document library for which content types have been enabled. Before you can achieve this, you need an additional piece of helper code to setup the document library to hold the proposal documents.
Listing 3 shows a ProposalLibrary class created for this purpose.
When creating the document library, you remove the default Document
content type so that users cannot create or upload standard documents. Finally,
you create a new instance of the ProjectProposal content type and
add it to the document library using the AddToList static method of
the ContentType helper class.
/// <summary>
/// A class that represents the proposals document library.
/// </summary>
class ProposalDocumentLibrary
{
SPDocumentLibrary m_docLib = null;
public ProposalDocumentLibrary(SPWeb web)
{
try {
SPListTemplate template =
web.ListTemplates["Document Library"];
System.Guid guid =
web.Lists.Add(
Strings._ProposalLibrary_Title,
Strings._ProposalLibrary_Desc,
template);
m_docLib = web.Lists[guid] as SPDocumentLibrary;
} catch {
}
// Initialize the base library properties.
m_docLib.OnQuickLaunch = true;
m_docLib.EnableVersioning = true;
m_docLib.EnableModeration = true;
m_docLib.EnableMinorVersions = true;
// Remove the default "Document" content type.
ContentType.RemoveFromList(m_docLib, "Document");
// Add the custom proposal content type.
ContentType.AddToList(m_docLib, new ProjectProposalType().Create(web));
}
}
The easiest way to deploy a new content type is to include it as part of a
custom feature. Here, you create a ProposalManagement feature to
enable all of the proposal management tools on a site. As part of the feature
implementation, you create an SPFeatureReceiver class for the
FeatureActivated event that handles the deployment details for our custom
content types. Listing 4 illustrates this process.
using System;
using System.Runtime.InteropServices;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;
namespace ProSharePoint2007
{
[Guid("63d38c9c-3ada-4e07-873f-a278443e910c")]
partial class <b>ProposalManagerFeature : SPFeatureReceiver</b>
{
[SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
public override void <b>FeatureActivated</b>(
SPFeatureReceiverProperties properties)
{
if (properties == null) {
return;
}
SPWeb web = properties.Feature.Parent as SPWeb;
// Create a library to hold the proposals and add a
// default list view to the left web part zone.
AddListViewWebPart(web,
new ProposalDocumentLibrary(web),
"Left", PartChromeType.Default);
}
/// <summary>
/// Creates a ListViewWebPart on the main page.
/// </summary>
private void AddListViewWebPart(SPWeb web, SPList list,
string zoneId,
PartChromeType chromeType)
{
// Access the default page of the web.
SPFile root = web.RootFolder.Files[0];
// Get the web part collection for the page.
SPLimitedWebPartManager wpm = root.GetLimitedWebPartManager(
System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared);
// Add a list view to the bottom of the zone.
ListViewWebPart part = new ListViewWebPart();
part.ListName = list.ID.ToString("B").ToUpper();
part.ChromeType = chromeType;
wpm.AddWebPart(part, zoneId, 99);
}
}
Now you have a site definition that includes the ProposalManager
feature. When a site is created based on this site definition, the
FeatureActivated event receiver creates a document library called Project
Proposals that is automatically associated with our Project Proposal content
type. Figure 4 shows the home page of a site created from the site definition.
Figure 4
Developing SharePoint 2007 Web Parts
BY JAN TIELENSWeb Parts are the building blocks of pages in SharePoint sites. Users of SharePoint sites can make use of those building blocks to determine what should be displayed on a specific page in a particular SharePoint site.
When you install SharePoint, you can make use of some out-of-the-box Web Parts straight away. Depending on whether you have Windows SharePoint Services (WSS) or Microsoft Office SharePoint Server (MOSS) as your SharePoint installation, you'll have more or less. Additionally, every SharePoint list and document library will have a Web Part counterpart that can display the contents of the corresponding list or document library.
Of course, the out-of-the-box Web Parts are not the only ones that you can use! Developers can build their own Web Parts as well and deploy them to the SharePoint server. End users won't notice the difference between the custom Web Parts and the out-of-the-box Web Parts, so Web Parts are a great way to extend SharePoint.
This article takes you through the basic steps to create your own Web Parts in various ways.
TIP
The techniques and technologies described in this article are applicable both to
Windows SharePoint Services (WSS) and Microsoft Office SharePoint Server (MOSS),
unless mentioned otherwise. So, in this article, mention of SharePoint
should be interpreted as Windows SharePoint Services 3.0 or Microsoft
Office SharePoint Server 2007.
TIP
Although you can create Web Parts making use of the SharePoint 2003 classes, and
use those Web Parts in SharePoint 2007 sites, you should make use of ASP.NET 2.0
Web Parts for your new projects.
Writing the Code
A Web Part in code is just a normal .NET class, nothing more, nothing less. In Visual Studio, you can make use of the Class Library project template to write code for the Web Part class. This code will be compiled into a .NET assembly, in this case a DLL that is exactly what you need. When Visual Studio is started, create a new project and select the Class Library template (Figure 1). The name of the new project is important, so think carefully when you choose a project name. Your project name should be unique on the server on which you would like to deploy your Web Parts. Additionally, the project name will be used later when the Web Parts are deployed. Also, be aware that names in .NET are case-sensitive!
Figure 1: Selecting the Class Library template
TIP
A common practice to ensure unique names in .NET environments is using
namespaces. If the project name is, for example, MVP.Book.WebParts,
by default, all the code will be sitting in the namespace with exactly the same
name.
Every Class Library project in Visual Studio can contain any number of Web Part classes, so think of the project as the container of your Web Parts.
When the new project is created, there will be one class already available:
Class1.cs. In general, Class1 is not a good name for a
Web Part, so rename the class to HelloWorld. The special thing
about a Web Part class is the fact that the class inherits from a specific base
WebPart class. This base WebPart class is available in
the System.Web assembly (Figure 2). By default, the Class Library
project doesn't contain a reference to this assembly, so you should add it
yourself. Right-click the project node in the Solutions Explorer window and
choose Add Reference. Next, select the System.Web assembly from the
list.
Figure 2: Locating the
System.Web assembly
Now the HelloWorld class can inherit from the WebPart
that is available in the System.Web.UI.WebControls.WebParts
namespace class.
TIP
To avoid having to type the full namespace of the WebPart base
class (and other classes as well), it's a common practice to add using
statements on top of your code. In this article, the common namespaces won't be
prefixed.
The resulting code is probably the most basic Web Part that can be created. It's
a Web Part that doesn't do anything. But because the HelloWorld
class inherited from the base WebPart class, the HelloWorld Web
Part already has a title bar and a border, as shown in Figure 3.
Figure 2: HelloWorld Web Part with title bar and border
TIP
All Web Part classes must be scoped public. A Web Part class that
is not marked as public can't be used in SharePoint Web Part pages.
In C#, when a new Class Library project is created, the default class
Class1 is automatically set to the public scope.
Unfortunately, classes that are added to the project are not scoped public
automatically; the developer must add the public keyword manually!
A Web Part developer must only focus on what should happen inside the Web Part.
The contents displayed inside the Web Part should be generated in the
Render method of the Web Part class. This method is already implemented
in the base WebPart class, so it should be overridden in the
HelloWorld WebPart class. This is the default implementation:
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
base.Render(writer);
}
The only parameter of the Render method is the writer
parameter, which is of the type HtmlTextWriter. You should use this
parameter to write HTML that will be rendered inside the Web Part. It's
important to write valid HTML with the writer parameter, because
invalid HTML is accepted as well, so it could break the Web Part page. The
HelloWorld Web Part should display fixed text, which is formatted as a title:
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
writer.Write("<h1>Web Parts Rock!</h1>");
}
TIP
It's not recommended to write HTML tags in plain strings as displayed in the
previous example. Later in this article, I discuss the proper way to generate
HTML. The purpose of this example is to show the real basics.
For now, the code is sufficient. It is a Web Part that displays fixed text. Before you can deploy this Web Part, the project must be built. So, the assembly (DLL) is generated. There are many ways to trigger the build process in Visual Studio, such as with the Build menu, using the Ctrl+Shift+B key combination, and so on.
The complete contents of the Class1.cs looks like this:
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls.WebParts;
namespace BasicWebParts
{
public class HelloWorld: WebPart
{
protected override void Render(
System.Web.UI.HtmlTextWriter writer)
{
writer.Write("<h1>Web Parts Rock!</h1>");
}
}
}
Using Controls in Web Parts
In the previous HelloWorld Web Part example, the Web Part code generated HTML by using a string. It is quite obvious that generating HTML for more complex user interfaces can be complicated. This section examines using controls in Web Parts to simplify the generation of HTML code in Web Parts:
The .NET Framework has lots of Web controls that can be used in Web Parts,
including Button, Textbox, Label, and so
on. Most of those user controls are residing in the
System.Web.UI.WebControls namespace. The advantage of these ASP.NET Web
controls is that they can generate HTML for themselves. You as a developer don't
have to focus on writing HTML to render a button, for example.
Simple Calculator Example
To illustrate the use of Web controls in Web Parts, let's write a very basic example that nicely shows how to make use of a couple of Web controls, including server-side code for the event handlers of those controls. The example used will be a basic Calculator Web Part: just two textboxes for entering two numeric values, two buttons for adding and subtracting those values, and a third textbox to display the result. The focus of this Web Part won't be how to validate the entered values, and so on, but rather to just show how to build the user interface and the server-side code.
Getting Started with the SimpleCalculator Class
A Web Part using Web controls is still a normal Web Part. So, in the
BasicWebParts Visual Studio project, add another class named
SimpleCalculator. First, add the following using statements
on top of the code:
using System.Web.UI.WebControls.WebParts; using System.Web.UI.WebControls; using System.Web.UI;
The using statements allow you to make use of the WebPart
base class and all the ASP.NET Web controls without including the full
namespace.
The second (and very important) addition to the code that must be made is the
public keyword for the SimpleCalculator class. By default, in a
Visual Studio 2005 C# Class Library project, the Class template doesn't include
the public keyword. If the Web Part class doesn't have this public keyword,
SharePoint won't be able to make use of it, so it must be added. The complete
SimpleCalculator class looks like this now:
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.WebControls;
using System.Web.UI;
namespace BasicWebParts
{
public class SimpleCalculator: WebPart
{
}
}
Declaring the Web Control Variables
For every Web control on the Web Part, a variable of that Web control should be declared as a member field. For the sample SimpleCalculator Web Part, there must be five variables registered (three textboxes and two buttons):
public class SimpleCalculator: WebPart
{
TextBox tbA;
TextBox tbB;
TextBox tbResult;
Button btnAdd;
Button btnSub;
}
Overriding the CreateChildControls Method
The next step to build the SimpleCalculator Web Part is to override the
CreateChildControls method from the base WebPart class. In
this method, you first create of all new instances for all the Web control
variables. Additionally, properties for those Web controls can be set as well.
Lastly, all the Web control instances should be added to the Controls
collection of the base WebPart class so that ASP.NET knows about
them, and, thus, the server-side events (if they exist) will be handled
correctly. Of course, it doesn't matter where the variables are instantiated and
added in the CreateChildControls method. It can be in the
beginning, but it can be at the end as well; they can be added even dynamically
in code. Personally, I prefer to structure my code as displayed here:
protected override void CreateChildControls()
{
tbA = new TextBox();
tbB = new TextBox();
tbResult = new TextBox();
btnAdd = new Button();
btnSub = new Button();
tbResult.ReadOnly = true;
btnAdd.Text = "+";
btnSub.Text = "-";
this.Controls.Add(tbA);
this.Controls.Add(tbB);
this.Controls.Add(btnAdd);
this.Controls.Add(btnSub);
this.Controls.Add(tbResult);
}
In the SimpleCalculator Web Part example, the ReadOnly property for
the results textbox is set to true, so end users won't be able to
change anything in that textbox. This is done from code. Also the text
properties of the buttons are set to "+" and "-".
The order in which the controls are added to the Controls
collection of the base WebPart class will determine how they are
rendered later on in the Web Part. For this example, the two input textboxes
should be rendered first, the two operation buttons should be rendered second,
and, finally, the textbox that should display the result should be rendered.
If the controls are added like this, one after another, all of them will appear
on one line. If the controls should be displayed on multiple lines, HTML break
tags () can be added in between, as shown here:
this.Controls.Add(tbA);
this.Controls.Add(new LiteralControl("<br>"));
this.Controls.Add(tbB);
this.Controls.Add(new LiteralControl("<br>"));
this.Controls.Add(btnAdd);
this.Controls.Add(btnSub);
this.Controls.Add(new LiteralControl("<br>"));
this.Controls.Add(tbResult);
Alternatively, HTML tables, layers, and so on can be used as a more flexible way to add layout to the Web Part.
Adding Event Handlers
At this point, the code doesn't do anything except render some Web controls. The
goal, of course, is to execute code when one of the operation buttons is
clicked. This can be accomplished by adding event handlers for the corresponding
events. These event handlers can be added in the CreateChildControls
method as well.
btnAdd.Click += new EventHandler(btnAdd_Click); btnSub.Click += new EventHandler(btnSub_Click);
The implementation of the event handlers can be done as follows. The
btnAdd_Click and btnSub_Click methods are members of the
SimpleCalculator class as well.
void btnAdd_Click(object sender, EventArgs e)
{
int a = int.Parse(tbA.Text);
int b = int.Parse(tbB.Text);
int c = a + b;
tbResult.Text = c.ToString();
}
void btnSub_Click(object sender, EventArgs e)
{
int a = int.Parse(tbA.Text);
int b = int.Parse(tbB.Text);
int c = a - b;
tbResult.Text = c.ToString();
}
TIP
Once again, the focus of this Web Part is not on how to validate the input of
the textboxes before the calculations are done. When this code is used in a
real-life scenario, adding validation code is obviously required.
The Full SimpleCalculator Code
Now, the SimpleCalculator class is finished, and the complete
combined code could look as follows:
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.WebControls;
using System.Web.UI;
namespace BasicWebParts
{
public class SimpleCalculator: WebPart
{
TextBox tbA;
TextBox tbB;
TextBox tbResult;
Button btnAdd;
Button btnSub;
protected override void CreateChildControls()
{
tbA = new TextBox();
tbB = new TextBox();
tbResult = new TextBox();
btnAdd = new Button();
btnSub = new Button();
tbResult.ReadOnly = true;
btnAdd.Text = "+";
btnSub.Text = "-";
btnAdd.Click += new EventHandler(btnAdd_Click);
btnSub.Click += new EventHandler(btnSub_Click);
this.Controls.Add(tbA);
this.Controls.Add(new LiteralControl("<br>"));
this.Controls.Add(tbB);
this.Controls.Add(new LiteralControl("<br>"));
this.Controls.Add(btnAdd);
this.Controls.Add(btnSub);
this.Controls.Add(new LiteralControl("<br>"));
this.Controls.Add(tbResult);
}
void btnAdd_Click(object sender, EventArgs e)
{
int a = int.Parse(tbA.Text);
int b = int.Parse(tbB.Text);
int c = a + b;
tbResult.Text = c.ToString();
}
void btnSub_Click(object sender, EventArgs e)
{
int a = int.Parse(tbA.Text);
int b = int.Parse(tbB.Text);
int c = a - b;
tbResult.Text = c.ToString();
}
}
}
The SimpleCalculator looks like Figure 4 at run-time.
Figure 4: SimpleCalculator Web Part
Manually Rendering the Web Controls
The user controls of the SimpleCalculator Web Part are rendered automatically on
the Web Part's user interface because the default implementation of the
Render method renders all items in the Control collection of
the base WebPart class. It's also possible to override this method
so that you are in full control of the rendering process. For every Web control,
the RenderControl method can be called to render the corresponding
HTML.
protected override void Render(HtmlTextWriter writer)
{
this.EnsureChildControls();
tbA.RenderControl(writer);
writer.Write("<br>");
tbB.RenderControl(writer);
writer.Write("<br>");
btnAdd.RenderControl(writer);
btnSub.RenderControl(writer);
writer.Write("<br>");
tbResult.RenderControl(writer);
}
If the preceding code is used, the HTML break tags don't have to be added to the
Controls collection. The CreateChildControls method
could look as follows:
protected override void CreateChildControls()
{
tbA = new TextBox();
tbB = new TextBox();
tbResult = new TextBox();
btnAdd = new Button();
btnSub = new Button();
tbResult.ReadOnly = true;
btnAdd.Text = "+";
btnSub.Text = "-";
btnAdd.Click += new EventHandler(btnAdd_Click);
btnSub.Click += new EventHandler(btnSub_Click);
this.Controls.Add(tbA);
this.Controls.Add(tbB);
this.Controls.Add(btnAdd);
this.Controls.Add(btnSub);
this.Controls.Add(tbResult);
}
Microsoft Corporation
September 2002
Applies to:
SharePoint Team Services™ from Microsoft®
Summary: Learn how to create a tool for updating multiple items in a list on a Web site based on SharePoint Team Services™ from Microsoft® by using Collaborative Application Markup Language (CAML), remote procedure call (RPC) protocol, and script. (20 printed pages)
Contents
Introduction
Creating the UI
Creating Arrays for Field Names and Types
Prepopulating the Value Picker
Updating the Items
Complete Code Sample
Introduction
You can use a combination of Collaborative Application Markup Language (CAML), remote procedure call (RPC) protocol, and script to create a tool for updating multiple items in a list on a Web site based on SharePoint Team Services™ from Microsoft®.
- CAML can be used in queries against the database to return fields or values.
- RPC provides a vehicle to communicate with FrontPage® Server Extensions from Microsoft in order to work with lists.
- Script can be used to construct the CAML and RPC strings that are posted to the server.
As in the sample for deleting items [ Articleimages/ms946723(printer).aspx ] , the XMLHTTP and DOMDocument objects are used to make posts and get the values returned from the server. This example implements much of the code in the sample for deleting items.
Note You must have Advanced Author permissions on the server to upload the samples and perform the required customizations presented in this article.
To update items based on user input, you must first post a CAML view to the server to get the identifiers (IDs) for the items that you want to update. After the IDs are returned, the following command must in turn be posted for each item ID:
http://[Server_Name]/[Subweb_Name]/_vti_bin/owssvr.dll?
Cmd=Save&List=[List_Name]&ID=[ID]&urn:schemas-microsoft-com:office:office#
[Field_Name]=[User_Value]
In addition to constructing and posting strings to update items, this sample shows how to create a form for the user interface (UI) that provides options for specifying two sets of criteria (the second set as optional), each with drop-down boxes for field and value pickers. The drop-down boxes are prepopulated to limit the user's need to type values. The user types replacement values in a text box, as illustrated in the following figure.
.gif)
Figure 1. User interface for updating items in a list
Note The example in this article assumes that you have installed the Microsoft XML Core Services (MSXML) 4.0, formerly called the Microsoft XML Parser, on the client. Download the Core Services [ http://msdn.microsoft.com/library/default.asp?url=/downloads/list/xmlgeneral.asp ] .
The code below can be put in an .htm file that also includes the required instructions for processing in SharePoint Team Services: a script block that writes the CAML HTMLBase element for relative links and a RedirectToFrame block that specifies the path to the page that the interpreter will process. In addition, this example assumes that you've added to the list's AllItems.htm page a link that passes the current list name as a parameter. For specific information on the formulation of these instructions and the link, see "Creating an AddItems.htm and its link" in Adding Multiple Items to an Existing List [ Articleimages/ms946719(printer).aspx ] . (Note that, unlike for the adding items sample in that article, it is not necessary to pass the view's URL to the sample presented here.)
Creating the UI
You can create a field picker, such as the following, that integrates CAML with
HTML to return the current list's fields and construct the drop-down box. The
onchange event of the field picker SELECT box calls the
PrePop function, to which it passes the field picker value, a reference
to the associated value picker, and the box's ID.
<FORM name="frmUpdate" ID="frmUpdate">
<TABLE class="ms-toolbar" cellpadding=5>
<TR><TD colspan=2 class=ms-sectionheader>Update Items</TD></TR>
<TR><TD COLSPAN=2>Select a field and the value to replace.</TD></TR>
</TABLE>
<TABLE class="ms-toolbar" CELLPADDING=5>
<TR><TD>Field:
<SELECT name="itmUpFld1" id="itmUpFld1"
onchange="JavaScript:PrePop(document.frmUpdate.itmUpFld1.value, lstItmUp1, id);">
<Option value="None" SELECTED>None
<ows:ForEach Select="/FIELDS/Field">
<Switch>
<Expr><Property Select="Type"/></Expr>
<Case Value="Computed"></Case>
<Case Value="Note"></Case>
<Case Value="URL"></Case>
<Default>
<Switch>
<Expr><Property Select="Name"/></Expr>
<Case Value="Modified"></Case>
<Case Value="Created"></Case>
<Case Value="Author"></Case>
<Case Value="Editor"></Case>
<Case Value="ID"></Case>
<Case Value="PercentComplete"></Case>
<Default>
<HTML><![CDATA[<OPTION value="]]></HTML>
<Property Select="Name"/>
<HTML><![CDATA[">]]></HTML>
<Property Select="DisplayName" HTMLEncode="TRUE"/>
</Default>
</Switch>
</Default>
</Switch>
</ows:ForEach>
</SELECT>
</TD></TR>
The CAML part enumerates the Fields collection for the list and extends the SELECT box by adding OPTION elements that represent each field. It uses two Switch statements. The first Switch statement evaluates each field's type and excludes Computed, Note, and URL field types.
- Computed fields are virtual fields that derive from actual fields contained in the database. A LinkTitle field, for example, displays a table's Title field value as a link, surrounding the value with an appropriate <A> tag. Consequently, Computed fields are excluded from the field picker, because they display values from actual fields, but also because they cannot be displayed properly in the picker.
- Note fields are excluded because they cannot be used within a CAML Where clause and will return a "Render Failed" error message.
- URL fields also cannot be implemented in the picker and are excluded.
The second Switch statement evaluates each field's name to exclude internal fields whose values cannot and should not be modified, and lays out the default rendering for other fields.
A skeletal drop-down list box can be added, which is populated based on user selection in the field picker.
<TR><TD>Value:
<SELECT name="lstItmUp1" id="lstItmUp1">
<Option value="None" SELECTED>None
</SELECT></TD></TR>
Next, you can add a box for the user to type in new values.
<TR><TD>Type the new value:
<INPUT TYPE="TEXT" ID="itmUp" VALUE=""></TD></TR>
The same code used here to define controls for a first set of criteria is used in the two controls of the second set, with the exception that in the second Switch statement, only the PercentComplete field need be excluded from the second field picker. Simply change the name attributes appropriately (here, to itmUpFld2 and lstItmUp2).
Finally, add an <A> tag whose href attribute calls the
EditItems function to update the items selected.
<TR><TD><A href="javascript:EditItems()" target=_self ID="UpdtItms" NAME="UpdtItms">Update Items</A></TD></TR> </TABLE> </FORM>
Creating Arrays for Field Names and Types
This sample assumes that a script block contains two arrays: one that associates each field name with a type and another that associates each field's internal name with its display name. See the Creating Arrays for Field Names and Types section of Deleting Multiple Items from a List [ Articleimages/ms946723(printer).aspx ] for a description of the example to implement here, or refer to the complete code sample at the end of this article.
Prepopulating the Value Picker
The PrePop function prepopulates the value picker based
on user selection through the field picker. This function constructs the CAML
view to post in order to return all the field's values from the database. It
uses XMLHTTP to post the view, and it uses the XML Document Object Model (DOM)
to parse the server's response and construct an array that contains the values
returned. For each value, it adds an OPTION element to the value picker. The
PrePop function also provides an added level of security
by limiting the amount of data that has to be typed by the user.
The code sample presented here for updating items makes extensive use of the
PrePop function described in the same section of
Deleting Multiple Items from a List [
Articleimages/ms946723(printer).aspx ] . The
following code for trapping cases where the first criteria set is incomplete can
be added near the beginning of the PrePop function in
place of the same trap that is used in deleting items.
function PrePop(selField, lstItem, itmID)
{
var strCol;
var lenItms = lstItem.children.length;
if ((itmID=="itmUpFld2") && (document.frmUpdate.itmUpFld1.value=="None"
|| document.frmUpdate.lstItmUp1.value=="None"))
{
alert("The first set of criteria is incomplete.");
document.frmUpdate.itmUpFld2.value="None";
RemChildren(lenItms, lstItem);
return;
}
RemChildren(lenItms, lstItem);
if (selField=="None")
{
return;
}
Other than the above trap, the remainder of the PrePop
function can be identical to the sample presented for deleting items (see
Deleting Multiple Items from a List [
Articleimages/ms946723(printer).aspx ] or the
complete code sample at the end).
Updating the Items
When the user selects a value in the UI and clicks Update Items, the
EditItems function is called. The
EditItems function uses XMLHTTP at two different points: first to return
item IDs based on user selection and then to post the URL string for updating
items. With only a few modifications, the same EditItems
function described in
Deleting Multiple Items from a List [
Articleimages/ms946723(printer).aspx ] can be used
here for updating items.
In place of the code used to assign control values to variables in the sample for deleting items—as well as the subsequent traps for invalid operators—you can use the following code for updating items.
function EditItems()
{
var selField1;
var selOp1;
var lstItm1;
var selField2;
var selOp2;
var lstItm2;
selField1 = GetSelectedValue(document.frmUpdate.itmUpFld1);
lstItm1 = document.frmUpdate.lstItmUp1.value;
selField2 = GetSelectedValue(document.frmUpdate.itmUpFld2);
lstItm2 = document.frmUpdate.lstItmUp2.value;
newItm = document.frmUpdate.itmUp.value;
selOp1 = "Eq";
selOp2 = "Eq";
Note that, for updating items, the operator used is always an Eq element.
The same code used in deleting items for trapping incomplete criteria and formulating parts of the Where clause can be used here (see Deleting Multiple Items from a List [ Articleimages/ms946723(printer).aspx ] —but in place of the code used for assembling the parts of the Where clause, the following code is used. First, the code checks whether a replacement value has been typed in the text box.
if (newItm=="")
{
alert("A replacement value has not been specified.");
return;
}
var wrnMultVal = "A field for a single item can equal only one value.";
if (document.frmUpdate.itmUpFld2.value != "None")
{
if (selField1==selField2)
{
alert(wrnMultVal);
return;
}
mkWhere = "<And>" + Where2 + "</And>";
}
The code for updating items, as for deleting items, also traps cases in which multiple values for the same field have been selected for an item. In the case of updating items, however, only "And" logic applies when both sets of criteria are specified. Other than this, the code is identical to the code used in the deleting items sample for constructing the complete string to post, and to the XML used to post the string and get the values returned from the server.
Issuing a Warning
The following section of code can be used in the EditItems
function to warn users that they're about to update items; it can replace the
equivalent section of the deleting items sample. Here, too, the message is
dynamically constructed for the user, depending on whether both sets of criteria
are being implemented.
var strWarn;
var strWarnUpdate = "Are you sure you want to replace " +
lstItm1 + " with " + document.frmUpdate.itmUp.value +
" for all items where the " + fldName[selField1] +
" field equals " + lstItm1;
if (selField2=="None")
{
strWarn = strWarnUpdate + "?";
}
else
{
strWarn = strWarnUpdate + " and the " + fldName[selField2] + " field equals " + " " + lstItm2 + "?";
}
Confirming Deletion
Finally, the following section for assembling and posting the URL command can be used in place of the warning and post formulation used in the deleting items sample.
if (confirm(strWarn))
{
for (i=0; i<arrCnt; i++)
{
var upString = "<ows:HttpVDir/>/_vti_bin/owssvr.dll?Cmd=Save" + "&List=<ows:List/>&ID=" +
arrEdit[i] + "&<ows:FieldPrefix/>" + selField1 + "=" + document.frmUpdate.itmUp.value;
HTTPobj2 = new ActiveXObject("Msxml2.XMLHTTP.4.0");
HTTPobj2.open("POST", upString, false);
HTTPobj2.send();
}
window.parent.location = "<ows:ListUrlDir/>" + "/AllItems.htm";
}
else
{
return;
}
As described in the article about deleting items, you need to include a function
for replacing HTML encoding (here, RepCAML) in the
strings being posted.
For more information about CAML, see the CAML reference in the SharePoint Team Services SDK [ http://msdn.microsoft.com/library/default.asp?url=/library/en-us/spsdk11/caml_schema/spxmlconcaml.asp ] .
Complete Code Sample
The following script block can be added to the <HEAD> of the page for updating items:
<SCRIPT language="JavaScript">
/*Construct an array that associates each field's name with that field's type. */
var fldType = new Array();
<ows:ForEach Select="/FIELDS/Field">
<Switch>
<Expr><Property Select="Type"/></Expr>
<Case Value="Computed"></Case>
<Default>
<HTML><![CDATA[fldType["]]></HTML>
<Property Select="Name"/>
<HTML><![CDATA["] = "]]></HTML>
<Property Select="Type"/>
<HTML><![CDATA[";]]></HTML>
</Default>
</Switch>
</ows:ForEach>
/*Construct an array that associates each field's internal name with its display name. */
var fldName = new Array;
<ows:ForEach Select="/FIELDS/Field">
<HTML><![CDATA[fldName["]]></HTML>
<Property Select="Name"/>
<HTML><![CDATA["] = "]]></HTML>
<Property Select="DisplayName" HTMLEncode="TRUE"/>
<HTML><![CDATA[";]]></HTML>
</ows:ForEach>
/*Use this function to prepopulate the value picker. */
function PrePop(selField, lstItem, itmID)
{
var strCol;
var lenItms = lstItem.children.length;
if ((itmID=="itmUpFld2") && (document.frmUpdate.itmUpFld1.value=="None" ||
document.frmUpdate.lstItmUp1.value=="None"))
{
alert("The first set of criteria is incomplete.");
document.frmUpdate.itmUpFld2.value="None";
RemChildren(lenItms, lstItem);
return;
}
RemChildren(lenItms, lstItem);
if (selField=="None")
{
return;
}
/*Create a different element for use in the mkWhere string, depending on
field name or field type.*/
if (fldType[selField]=="Lookup")
{
strCol = "<LookupColumn Name=\"" + selField + "\"/>";
}
else if (fldType[selField]=="DateTime")
{
strCol = "<Column Format=\"ISO8601\"/>";
}
else
{
strCol = "<Column/>";
}
/*Complete construction of the string for posting a CAML view to the
server to return values for the selected field. Use -,- as separator between
values. Replace entities with angle brackets and quotation marks.*/
var strMk = "<ows:HttpVDir/>/_vti_bin/owssvr.dll?Cmd=DisplayPost" +
"&PostBody=<ows:Batch><Method ID="itmIDs1" +
""><SetList><ows:List/></SetList>" +
"<View><ViewFields><FieldRef Name=\"" +
selField + "\"/></ViewFields><ViewBody>" +
"<Fields>" + strCol + "<HTML><![CDATA[-,-]]>" +
"</HTML></Fields></ViewBody><Query>" +
"<OrderBy><FieldRef Name=\"" +
selField + "\" Ascending=\"FALSE\"/></OrderBy>" +
"</Query></View></Method></ows:Batch>";
var strMk = RepCAML(strMk);
/*Create XMLHTTP and DOMDocument objects for posting the view to the server and parsing the response.*/
var HTTPob = new ActiveXObject("Msxml2.XMLHTTP.4.0");
HTTPob.open("POST", strMk, false);
HTTPob.send();
var docObjRec = new ActiveXObject("Msxml2.DOMDocument.4.0");
docObjRec.async = false;
docObjRec.loadXML(HTTPob.responseXML.xml);
docObjRec.setProperty("SelectionLanguage", "XPath");
var valRet = docObjRec.selectSingleNode("//Results/Result");
var strRet = valRet.text;
/*Remove the extra separator at the end and create an array to contain the values. */
var Len1 = strRet.length - 3;
strRetIDs = strRet.substr(0, Len1);
var arIDs = strRetIDs.split(/-,-/);
var aCnt = arIDs.length;
/*Using the number of elements in the array, iterate through all values and
create OPTION elements to add to the value picker. When array elements
repeat, do nothing.*/
for (i=0; i<aCnt; i++)
{
if (arIDs[i]==arIDs[i+1])
{}
else
{
var newItem = document.createElement("OPTION");
lstItem.children(0).insertAdjacentElement("afterEnd",newItem);
newItem.innerText = arIDs[i];
strValue = arIDs[i].toString();
var rep = /\$/;
strVal = strValue.replace(rep,"");
lstItem.style.width = "1";
newItem.setAttribute("value", strVal);
lstItem.style.width = "auto";
}
}
}
/*Remove options from the value picker when changes are made in the field picker. */
function RemChildren(lenChlds, lstItm)
{
for (i=1; i<lenChlds; i++)
{
var remChld = lstItm.children[1];
lstItm.removeChild(remChld);
}
}
/*Get the user-entered data.*/
function GetSelectedValue(frmElem)
{
if (frmElem)
{
itmVal = frmElem.options[frmElem.selectedIndex].value;
return itmVal;
}
else
return "";
}
/*Use this function to get IDs and delete items. Start by assigning user values to variables.*/
function EditItems()
{
var selField1;
var selOp1;
var lstItm1;
var selField2;
var selOp2;
var lstItm2;
selField1 = GetSelectedValue(document.frmUpdate.itmUpFld1);
lstItm1 = document.frmUpdate.lstItmUp1.value;
selField2 = GetSelectedValue(document.frmUpdate.itmUpFld2);
lstItm2 = document.frmUpdate.lstItmUp2.value;
newItm = document.frmUpdate.itmUp.value;
selOp1 = "Eq";
selOp2 = "Eq";
/*Trap cases where the field or value picker equals None. */
if (selField1=="None" || lstItm1=="None")
{
alert("The first set of criteria is incomplete.");
return;
}
if ((selField2!="None" && lstItm2=="None") ||
(lstItm1=="None" && lstItm2!="None"))
{
alert("The second set of criteria is incomplete.");
return;
}
/*Construct the operators specified by the user and create FieldRef elements. */
var Op1 = "<" + selOp1 + ">";
var Op2 = "</" + selOp1 + ">";
var Op3 = "<" + selOp2 + ">";
var Op4 = "</" + selOp2 + ">";
var Fld1 = "<FieldRef Name="" + selField1 + ""/>";
var Fld2 = "<FieldRef Name="" + selField2 + ""/>";
var mkWhere;
var Where1;
var Where2;
/*Construct core parts of the Where clause for posting the CAML view.*/
Where1 = Op1 + Fld1 + "<Value Type="" + fldType[selField1] +
"">" + lstItm1 + "</Value>" + Op2;
Where2 = Where1 + Op3 + Fld2 + "<Value Type="" +
fldType[selField2] + "">" + lstItm2 + "</Value>" + Op4;
/*If the second criteria set is not used, create the shorter Where1 clause. */
if (selField2 == "None")
{
mkWhere = Where1;
}
/*Trap cases where the replacement value box is left empty. If the second criteria set is used,
create the appropriate Where clause, but add handling for cases in which the same field is
selected in both field pickers. Create the longer Where clause.*/
if (newItm=="")
{
alert("A replacement value has not been specified.");
return;
}
var wrnMultVal = "A field for a single item can equal only one value.";
if (document.frmUpdate.itmUpFld2.value != "None")
{
if (selField1==selField2)
{
alert(wrnMultVal);
return;
}
mkWhere = "<And>" + Where2 + "</And>";
}
/*Assemble the complete string to be posted for returning the IDs of items to
update, and then remove HTML encoding.*/
var strParam = "<ows:HttpVDir/>/_vti_bin/owssvr.dll?" +
"Cmd=DisplayPost&PostBody=<ows:Batch><Method " +
"ID="Del1"><SetList><ows:List/>" +
"</SetList><View DisplayName="MyHiddenView" +
""><ViewFields>" +
"<FieldRef Name="ID"/></ViewFields>" +
"<Query><Where>" + mkWhere + "</Where>" +
"<OrderBy><FieldRef Name="ID"/>" +
"</OrderBy></Query><ViewBody>" +
"<Fields> <Field/>" +
"<HTML><![CDATA[,]]></HTML>" +
"</Fields></ViewBody></View>" +
"</Method></ows:Batch>";
var strPost = RepCAML(strParam);
/*Use XML HTTP to post the string to the server, and then create a DOM
document object to contain and parse the server's response.*/
var docObjReceive;
var HTTPobj;
var strReturn;
var strIDs;
var Len;
HTTPobj = new ActiveXObject("Msxml2.XMLHTTP.4.0");
HTTPobj.open("POST", strPost, false);
HTTPobj.send();
docObjReceive = new ActiveXObject("Msxml2.DOMDocument.4.0");
docObjReceive.async = false;
docObjReceive.loadXML(HTTPobj.responseXML.xml);
docObjReceive.setProperty("SelectionLanguage", "XPath");
strReturn = docObjReceive.selectSingleNode("//Results/Result");
strRaw = strReturn.text;
/*Create an array to contain IDs of items to update. */
Len = strRaw.length - 1;
strIDs = strRaw.substr(0, Len);
if (strIDs=="")
{
alert("Sorry, no items matching the specified criteria were found.");
return;
}
var arrEdit = strIDs.split(/,/);
var arrCnt = arrEdit.length;
/*Offer a warning to the user before modifying the items.*/
var strWarn;
var strWarnUpdate = "Are you sure you want to replace " +
lstItm1 + " with " + document.frmUpdate.itmUp.value +
" for all items where the " + fldName[selField1] +
" field equals " + lstItm1;
if (selField2=="None")
{
strWarn = strWarnUpdate + "?";
}
else
{
strWarn = strWarnUpdate + " and the " + fldName[selField2] +
" field equals " + " " + lstItm2 + "?";
}
/*If the update is confirmed, build a complete update string to post
for updating each item and post the string to the server.*/
if (confirm(strWarn))
{
for (i=0; i<arrCnt; i++)
{
var upString = "<ows:HttpVDir/>/_vti_bin/owssvr.dll?Cmd=" +
"Save&List=<ows:List/>&ID=" + arrEdit[i] +
"&<ows:FieldPrefix/>" + selField1 +
"=" + document.frmUpdate.itmUp.value;
HTTPobj2 = new ActiveXObject("Msxml2.XMLHTTP.4.0");
HTTPobj2.open("POST", upString, false);
HTTPobj2.send();
}
window.parent.location = "<ows:ListUrlDir/>" + "/AllItems.htm";
}
else
{
return;
}
}
/*Replace HTML encoding.*/
function RepCAML(theStr)
{
var re;
re = /</g;
theStr=theStr.replace(re,"<");
re = />/g;
theStr=theStr.replace(re,">");
re = /"/g;
theStr=theStr.replace(re,"\"");
return(theStr);
}
</SCRIPT>
The following form can be placed in the <BODY> section of the page.
<FORM name="frmUpdate" ID="frmUpdate">
<TABLE class="ms-toolbar" cellpadding=5>
<TR><TD colspan=2 class=ms-sectionheader>Update Items</TD></TR>
<TR><TD COLSPAN=2>Select a field and the value to replace.</TD></TR>
</TABLE>
<TABLE class="ms-toolbar" CELLPADDING=5>
<TR><TD>Field: <SELECT name="itmUpFld1" id="itmUpFld1"
onchange="JavaScript:PrePop(document.frmUpdate.itmUpFld1.value, lstItmUp1, id);">
<Option value="None" SELECTED>None
<ows:ForEach Select="/FIELDS/Field">
<Switch>
<Expr><Property Select="Type"/></Expr>
<Case Value="Computed"></Case>
<Case Value="Note"></Case>
<Case Value="URL"></Case>
<Default>
<Switch>
<Expr><Property Select="Name"/></Expr>
<Case Value="Modified"></Case>
<Case Value="Created"></Case>
<Case Value="Author"></Case>
<Case Value="Editor"></Case>
<Case Value="ID"></Case>
<Case Value="PercentComplete"></Case>
<Default>
<HTML><![CDATA[<OPTION value="]]></HTML>
<Property Select="Name"/>
<HTML><![CDATA[">]]></HTML>
<Property Select="DisplayName" HTMLEncode="TRUE"/>
</Default>
</Switch>
</Default>
</Switch>
</ows:ForEach>
</SELECT>
</TD></TR><TR>
<TD>Value: <SELECT name="lstItmUp1" id="lstItmUp1">
<Option value="None" SELECTED>None
</SELECT></TD></TR>
<TR><TD>Type the new value: <INPUT TYPE="TEXT" ID="itmUp" VALUE=""></TD></TR>
<TR><TD COLSPAN=2>To specify the item more precisely, select an additional
set of criteria.</TD></TR> <TR><TD>Field: <SELECT name="itmUpFld2" id="itmUpFld2"
onchange="JavaScript:PrePop(document.frmUpdate.itmUpFld2.value, lstItmUp2, id);">
<Option value="None" SELECTED>None
<ows:ForEach Select="/FIELDS/Field">
<Switch>
<Expr><Property Select="Type"/></Expr>
<Case Value="Computed"></Case>
<Case Value="Note"></Case>
<Case Value="URL"></Case>
<Default>
<Switch>
<Expr><Property Select="Name"/></Expr>
<Case Value="PercentComplete"></Case>
<Default>
<HTML><![CDATA[<OPTION value="]]></HTML>
<Property Select="Name"/>
<HTML><![CDATA[">]]></HTML>
<Property Select="DisplayName" HTMLEncode="TRUE"/>
</Default>
</Switch>
</Default>
</Switch>
</ows:ForEach>
</SELECT></TD></TR>
<TR><TD>Value: <SELECT name="lstItmUp2" id="lstItmUp2">
<Option value="None" SELECTED>None
</SELECT></TD></TR>
<TR><TD><BR></TD></TR>
<TR><TD><A href="javascript:EditItems(document.all.UpdtItms.id)" target=_self
ID="UpdtItms" NAME="UpdtItms">Update Items</A></TD></TR> </TABLE> </FORM>
Summary: Explore details of developing a custom membership and role provider for Microsoft Office SharePoint Server (MOSS) 2007 and Windows SharePoint Services 3.0, including the minimum required interfaces and how to register and debug your custom provider. This article is part 2 of 3. (26 printed pages)
Steve Peschka, Microsoft Corporation
December 2007
Applies to: Microsoft Office SharePoint Server 2007, Windows SharePoint Services 3.0
Contents
-
Minimum Interfaces Required by MOSS and Windows SharePoint Services
-
Using Web Services with a Site Protected by Forms Authentication
Read part 1 and part 3:
Forms Authentication in SharePoint Products and Technologies (Part 1): Introduction [ Articleimages/bb975136(printer).aspx ]
Forms Authentication in SharePoint Products and Technologies (Part 3): Forms Authentication vs. Windows Authentication [ Articleimages/bb977430(printer).aspx ]
Developing Custom Membership and Role Providers
Microsoft Office SharePoint Server (MOSS) 2007 and Windows SharePoint Services 3.0 (in this article series, collectively referred to as SharePoint Products and Technologies) are built upon the ASP.NET 2.0 Framework. As such, support for forms authentication extends not only to the membership and role providers that are included with ASP.NET, MOSS, and Windows SharePoint Services, but also to custom membership and role providers. The custom provider is required to inherit only from the ASP.NET membership or role base class respectively, and to implement a limited set of interfaces on those classes.
Note:
|
|---|
|
Addressing this part of the Microsoft .NET Framework extensively is beyond the scope of this article. For more information about the membership and role provider base classes, see System.Web.Security.Membership [ Articleimages/dazakw52(printer).aspx ] and System.Web.Security.RoleProvider [ Articleimages/kkdz3641(printer).aspx ] . |
Applying the XML File for the Data Source
We now describe how to write a custom provider for users and roles that uses an XML file for the directory information. The format of the XML file looks like the following code.
Important:
|
|---|
|
It is NOT a safe or secure practice to include unencrypted passwords in a clear text file, as displayed in the following code example. We are using it in this case only to simplify the explanation of developing custom providers. This is not an acceptable design for a production application. When storing sensitive information in a configuration file for an application, you should encrypt the sensitive values by using Protected Configuration. For more information, see Encrypting Configuration Information Using Protected Configuration [ Articleimages/53tyfkaw(printer).aspx ] and Securing Membership [ Articleimages/ms178398(printer).aspx ] . If you take the approach used as an example in this article, you are responsible for encrypting and decrypting the data within your provider. |
<Users> <User name="user1" email="user1@microsoft.com" password="test" created="7/15/2007"> <Groups> <Group>Administrators</Group> <Group>Authors</Group> <Group>Readers</Group> </Groups> </User> <User name="user2" email="user2@microsoft.com" password="test" created="7/31/2007"> <Groups> <Group>Readers</Group> </Groups> </User> <User name="user3" email="user3@microsoft.com" password="test" created="8/15/2007"> <Groups> <Group>Designers</Group> <Group>Readers</Group> </Groups> </User> </Users>
Inheriting from the Membership and Role Base Classes
The base class that is used for membership is System.Web.Security.MembershipProvider [ Articleimages/sfka4yf8(printer).aspx ] ; if you are writing a custom membership provider you must inherit from this class.
The base class that is used for roles is System.Web.Security.RoleProvider; if you are writing a custom role provider you must inherit from this class.
using System.Web; using System.Web.Security; using System.Xml; using System.Configuration; namespace Microsoft.IW { public class customUser : MembershipProvider { // This is not necessary for every provider; we are doing // it for this one provider so that we can ensure our XML // file is set up correctly. private XmlDocument xDoc = null; // Code in here. } public class customRole : RoleProvider { // This is not necessary for every provider; we are doing // it for this one provider so that we can ensure our XML // file is set up correctly. private XmlDocument xDoc = null; //Code in here. } }
Imports System.Web Imports System.Web.Security Imports System.Xml Imports System.Configuration Namespace Microsoft.IW Public Class customUser Inherits MembershipProvider ' This is not necessary for every provider; we are doing ' it for this one provider so that we can ensure our XML ' file is set up correctly. Private xDoc As XmlDocument = Nothing ' Code in here. End Class Public Class customRole Inherits RoleProvider ' This is not necessary for every provider; we are doing ' it for this one provider so that we can ensure our XML ' file is set up correctly. Private xDoc As XmlDocument = Nothing ' Code in here. End Class End Namespace
Minimum Interfaces Required by MOSS and Windows SharePoint Services
Both the MembershipProvider class and the RoleProvider class include several methods and properties. To use the provider, only a subset of those is required by MOSS and Windows SharePoint Services.
To use the MembershipProvider, you must implement the following methods:
-
System.Web.Security.MembershipProvider.GetUser [ Articleimages/ms152043(printer).aspx ]
-
System.Web.Security.MembershipProvider.GetUserNameByEmail(System.String) [ Articleimages/57hsxfsd(printer).aspx ]
-
System.Web.Security.MembershipProvider.ValidateUser(System.String,System.String) [ Articleimages/05d03b82(printer).aspx ]
-
System.Web.Security.MembershipProvider.FindUsersByEmail(System.String,System.Int32,System.Int32,System.Int32) [ Articleimages/96e54ch9(printer).aspx ]
-
System.Web.Security.MembershipProvider.FindUsersByName(System.String,System.Int32,System.Int32,System.Int32) [ Articleimages/89e8kx21(printer).aspx ]
To use the RoleProvider, you must implement the following methods:
-
System.Web.Security.RoleProvider.GetRolesForUser(System.String) [ Articleimages/wzdyk8sx(printer).aspx ]
-
System.Web.Security.RoleProvider.RoleExists(System.String) [ Articleimages/5t22kc66(printer).aspx ]
Note:
|
|---|
|
The following section contains several code examples meant to demonstrate only the implementation of the minimum interfaces required to have a custom membership and role provider that is usable with MOSS 2007 and Windows SharePoint Services 3.0. They are not meant to demonstrate coding best practices. |
In addition to the methods that SharePoint Products and Technologies require, the class contract requires you to implement some additional methods and properties. In those cases, you can either implement the functionality or choose to throw a System.NotSupportedException [ Articleimages/8a7a4e64(printer).aspx ] . For example, the MembershipProvider requires a System.Web.Security.MembershipProvider.ChangePassword(System.String,System.String,System.String) [ Articleimages/bdt44e91(printer).aspx ] method; if you do not want to support that functionality, your override of that method should resemble the following code.
public override bool ChangePassword(string username, string oldPassword, string newPassword) { throw new NotSupportedException(); }
Public Overrides Function ChangePassword(ByVal username As String, ByVal oldPassword As String,
ByVal newPassword As String) As Boolean Throw New NotSupportedException() End Function
As with most custom providers, this example includes special code to work with the data store: in this case, an XML file. A simple helper class was developed to read the file from a hard-coded location on disk and store it in cache. The item was added into cache with a dependency so that if the file on disk changes, such as when a user is added or removed, the XML file is purged from cache. Following is the code for the helper class.
using System; using System.Collections.Generic; using System.Text; using System.Xml; using System.Diagnostics; using System.Web; namespace Microsoft.IW { internal class Helper { private const string XML_PATH = "c:\\inetpub\\wwwroot\\userdata\\users.xml"; private const string XML_CACHE = "xmlUserDocFile"; public static XmlDocument GetXmlFile() { // This helper function looks for the XML document // in cache; if it is there, // it pulls it out of cache. Otherwise, it loads // it into an XML document // and stores it in cache with a file cache // dependency, so that if the // the XML file changes it will be flushed out // of cache and have to be reread and reloaded. XmlDocument xDoc = null; try { // Look for the item in cache. if (HttpContext.Current.Cache[XML_CACHE] == null) { // Create a new document. xDoc = new XmlDocument(); // Load it from disk. xDoc.Load(XML_PATH); // Save it to cache. HttpContext.Current.Cache.Insert(XML_CACHE, xDoc, new System.Web.Caching.CacheDependency(XML_PATH)); } else xDoc = (XmlDocument)HttpContext.Current.Cache[XML_CACHE]; } catch (Exception ex) { // Try writing to the event log. try { EventLog.WriteEntry("fbaSharp Custom Provider", "Error loading user xml file: " + ex.Message, EventLogEntryType.Error); } catch { // Ignore. } } return xDoc; } } }
Imports System.Xml Imports System.Diagnostics Imports System.Web Namespace Microsoft.IW Friend Class Helper Private Const XML_PATH As String = _ "c:\inetpub\wwwroot\userdata\users.xml" Private Const XML_CACHE As String = "xmlUserDocFile" Public Shared Function GetXmlFile() As XmlDocument ' This helper function looks for the XML document ' in cache; if it is there, it pulls it out of cache. ' Otherwise, it loads it into an XML document and ' stores it in cache with a file cache dependency, ' so that if the XML file changes it will be ' flushed out of cache and have to be reread ' and reloaded. Dim xDoc As XmlDocument = Nothing Try ' Look for the item in cache. If HttpContext.Current.Cache(XML_CACHE) Is Nothing Then ' Create a new document. xDoc = New XmlDocument ' Load it from disk. xDoc.Load(XML_PATH) ' Save it to cache. HttpContext.Current.Cache.Insert(XML_CACHE, xDoc, _ New System.Web.Caching.CacheDependency(XML_PATH)) Else xDoc = HttpContext.Current.Cache(XML_CACHE) End If Catch ex As Exception ' Try writing to event log. Try EventLog.WriteEntry("fbaVB Custom Provider", _ "Error loading user xml file: " & _ ex.Message, EventLogEntryType.Error) Catch logEx As Exception ' Ignore. End Try End Try Return xDoc End Function End Class End Namespace
Functions Required in a Custom Membership Provider
The following sections describe the functions (methods) required in a custom membership provider.
GetUser Method
The GetUser function has two overridden implementations and returns a System.Web.Security.MembershipUser [ Articleimages/d1b506ez(printer).aspx ] object based on user name and a flag that indicates whether a user is online. As with any class, the author must determine whether and how to use the parameters that are passed to it. For example, in most cases it is unlikely that you would change the return value from this function based only on whether the user is online. Notice that the providerUserKey is useful only if you are using a single Membership database to store users for multiple applications and you need to distinguish in which of those applications the user exists. You can find more detail about this parameter on MSDN in the Membership classes materials that are referenced at the beginning of this article. This parameter is controlled in the applicationName attribute of the add element in the web.config file that is used to define a membership provider.
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline) { MembershipUser ret = null; XmlNode xNode = null; try { // Get the XML document. xDoc = Helper.GetXmlFile(); // Proceed if it contains something. if (xDoc != null) { // Look for a user with a name that matches. xNode = xDoc.SelectSingleNode("/Users/User[@name='" + providerUserKey.ToString() + "']"); // Determine whether there are any matches. if (xNode != null) { // Create a new membershipusercollection. ret = new MembershipUser(Membership.Provider.Name, xNode.Attributes["name"].Value.ToString(), xNode.Attributes["name"].Value.ToString(), xNode.Attributes["email"].Value.ToString(), string.Empty, string.Empty, true, false, DateTime.Parse(xNode.Attributes["created"].Value.ToString()), DateTime.Today, DateTime.Today, DateTime.Today, DateTime.MinValue); } } } catch { // Take appropriate action. } // Return the results. return ret; } public override MembershipUser GetUser(string username, bool userIsOnline) { MembershipUser ret = null; XmlNode xNode = null; try { // Get the XML document. xDoc = Helper.GetXmlFile(); // Proceed if it contains something. if (xDoc != null) { // Look for a user with a name that matches. xNode = xDoc.SelectSingleNode("/Users/User[@name='" + username + "']"); // See if there are any matches. if (xNode != null) { // Create a new membershipusercollection. ret = new MembershipUser(Membership.Provider.Name, xNode.Attributes["name"].Value.ToString(), xNode.Attributes["name"].Value.ToString(), xNode.Attributes["email"].Value.ToString(), string.Empty, string.Empty, true, false, DateTime.Parse(xNode.Attributes["created"].Value.ToString()), DateTime.Today, DateTime.Today, DateTime.Today, DateTime.MinValue); } } } catch { // Take appropriate action. } // Return the results. return ret; }
Public Overloads Overrides Function GetUser(_ ByVal providerUserKey As Object, ByVal userIsOnline As Boolean) _ As System.Web.Security.MembershipUser Dim ret As MembershipUser = Nothing Dim xNode As XmlNode = Nothing Try ' Get the XML document. xDoc = Helper.GetXmlFile() ' Proceed if it contains something. If xDoc IsNot Nothing Then ' Look for a user with a name that matches. xNode = xDoc.SelectSingleNode("/Users/User[@name='" & _ providerUserKey.ToString() & "']") ' See if there are any matches. If xNode IsNot Nothing Then 'Create a new membershipusercollection. ret = New MembershipUser(Membership.Provider.Name, _ xNode.Attributes("name").Value.ToString(), _ xNode.Attributes("name").Value.ToString(), _ xNode.Attributes("email").Value.ToString(), _ String.Empty, String.Empty, True, False, _ Date.Parse(xNode.Attributes("created").Value.ToString()), _ Date.Today, Date.Today, Date.Today, Date.MinValue) End If End If Catch ex As Exception 'Take appropriate action. End Try ' Return the results. Return ret End Function Public Overloads Overrides Function GetUser( _ ByVal username As String,ByVal userIsOnline As Boolean) _ As System.Web.Security.MembershipUser Dim ret As MembershipUser = Nothing Dim xNode As XmlNode = Nothing Try ' Get the XML document. xDoc = Helper.GetXmlFile() ' Proceed if it contains something. If xDoc IsNot Nothing Then ' Look for a user with a name that matches. xNode = xDoc.SelectSingleNode("/Users/User[@name='" & _ username & "']") ' See if there are any matches. If xNode IsNot Nothing Then ' Create a new membershipusercollection. ret = New MembershipUser(Membership.Provider.Name, _ xNode.Attributes("name").Value.ToString(), _ xNode.Attributes("name").Value.ToString(), _ xNode.Attributes("email").Value.ToString(), _ String.Empty, String.Empty, True, False, _ Date.Parse(xNode.Attributes("created").Value.ToString()), _ Date.Today, Date.Today, Date.Today, Date.MinValue) End If End If Catch ex As Exception 'Take appropriate action. End Try ' Return the results. Return ret End Function
GetUserNameByEmail Method
The GetUserNameByEmail function takes an e-mail address as a parameter and looks for a user with that e-mail address. If it finds such a user, it returns the user name.
public override string GetUserNameByEmail(string email) { string ret = string.Empty; XmlNode xNode = null; try { // Get the XML document. xDoc = Helper.GetXmlFile(); // Proceed if it contains something. if (xDoc != null) { // Look for the user. xNode = xDoc.SelectSingleNode("/Users/User[@email='" + email + "']"); // See if it found a match. if (xNode != null) ret = xNode.Attributes["name"].Value.ToString(); } } catch { // Take appropriate action. } // Return the results. return ret; }
Public Overrides Function GetUserNameByEmail(ByVal email As String) _ As String Dim ret As String = String.Empty Dim xNode As XmlNode = Nothing Try ' Get the XML document. xDoc = Helper.GetXmlFile() ' Proceed if it contains something. If xDoc IsNot Nothing Then ' Look for the user. xNode = xDoc.SelectSingleNode("/Users/User[@email='" & _ email & "']") ' See if it found a match. If xNode IsNot Nothing Then _ ret = xNode.Attributes("name").Value.ToString End If Catch ex As Exception ' Take appropriate action. End Try ' Return the results. Return ret End Function
ValidateUser Method
The ValidateUser function takes a user name and password and verifies whether it is correct.
public override bool ValidateUser(string username, string password) { bool ret = false; XmlNode xNode = null; try { // Get the XML document. xDoc = Helper.GetXmlFile(); // Proceed if it contains something. if (xDoc != null) { // Look for the user. xNode = xDoc.SelectSingleNode("/Users/User[@name='" + username + "']"); // See if it found a match. if (xNode != null) { // Look for the password attribute to see if it matches. if (xNode.Attributes["password"].Value.ToString() == password) ret = true; } } } catch { // Take appropriate action. } // Return the results. return ret; }
Public Overrides Function ValidateUser(ByVal username As String, _ ByVal password As String) As Boolean Dim ret As Boolean = False Dim xNode As XmlNode = Nothing Try ' Get the XML document. xDoc = Helper.GetXmlFile() ' Proceed if it contains something. If xDoc IsNot Nothing Then ' Look for the user. xNode = xDoc.SelectSingleNode("/Users/User[@name='" & _ username & "']") ' See if it found a match. If xNode IsNot Nothing Then ' Look for the password attribute to see if it matches. If xNode.Attributes("password").Value.ToString = password Then _ ret = True End If End If Catch ex As Exception ' Take appropriate action. End Try ' Return the results. Return ret End Function
FindUsersByEmail Method
The FindUsersByEmail function takes an e-mail address as a parameter and finds all users whose e-mail addresses start with that value. It returns a System.Web.Security.MembershipUserCollection [ Articleimages/3xe386wc(printer).aspx ] object.
public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { MembershipUserCollection ret = null; XmlNodeList xList = null; // Initialize the number of records found. totalRecords = 0; try { // Get the XML document. xDoc = Helper.GetXmlFile(); // Proceed if it contains something. if (xDoc != null) { // Look for users with matching e-mail addresses. xList = xDoc.SelectNodes("/Users/User[starts-with(@email, '" + emailToMatch + "')]"); // See if there are any matches. if ((xList != null) && (xList.Count > 0)) { // Set the number of records found. totalRecords = xList.Count; // Create a new membershipusercollection. ret = new MembershipUserCollection(); // Enumerate each match and add it to the collection. foreach (XmlNode xNode in xList) { ret.Add(new MembershipUser(Membership.Provider.Name, xNode.Attributes["name"].Value.ToString(), xNode.Attributes["name"].Value.ToString(), xNode.Attributes["email"].Value.ToString(), string.Empty, string.Empty, true, false, DateTime.Parse(xNode.Attributes["created"].Value.ToString()), DateTime.Today, DateTime.Today, DateTime.Today, DateTime.MinValue)); } } } } catch { // Take appropriate action. } // Return the results. return ret; }
Public Overrides Function FindUsersByEmail(ByVal emailToMatch As _ String, ByVal pageIndex As Integer, ByVal pageSize As Integer, _ ByRef totalRecords As Integer) As _ System.Web.Security.MembershipUserCollection Dim ret As MembershipUserCollection = Nothing Dim xList As XmlNodeList = Nothing ' Initialize the number of records found. totalRecords = 0 Try ' Get the XML document. xDoc = Helper.GetXmlFile() ' Proceed if it contains something. If xDoc IsNot Nothing Then ' Look for users with matching e-mail addresses. xList = xDoc.SelectNodes("/Users/User[starts-with(@email, '" & _ emailToMatch & "')]") ' See if there are any matches. If xList IsNot Nothing AndAlso xList.Count > 0 Then ' Set the number of records found. totalRecords = xList.Count ' Create a new membershipusercollection. ret = New MembershipUserCollection() ' Enumerate each match and add it to the collection. For Each xNode As XmlNode In xList ret.Add(New MembershipUser(Membership.Provider.Name, _ xNode.Attributes("name").Value.ToString(), _ xNode.Attributes("name").Value.ToString(), _ xNode.Attributes("email").Value.ToString(), _ String.Empty, String.Empty, True, False, _ Date.Parse(xNode.Attributes("created").Value.ToString()), _ Date.Today, Date.Today, Date.Today, Date.MinValue)) Next End If End If Catch ex As Exception ' Take appropriate action. End Try ' Return the results. Return ret End Function
FindUsersByName Method
The FindUserByName function works the same way that the FindUserByEmail function does; however, it takes a user name (full or partial) as a parameter.
public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { MembershipUserCollection ret = null; XmlNodeList xList = null; // Initialize the number of records found. totalRecords = 0; try { // Get the XML document. xDoc = Helper.GetXmlFile(); // Proceed if it contains something. if (xDoc != null) { // Look for users with matching e-mail addresses. xList = xDoc.SelectNodes("/Users/User[starts-with(@name, '" + usernameToMatch + "')]"); // See if there are any matches. if ((xList != null) && (xList.Count > 0)) { // Create a new membershipusercollection. ret = new MembershipUserCollection(); // Enumerate each match and add it to the collection. foreach (XmlNode xNode in xList) { ret.Add(new MembershipUser(Membership.Provider.Name, xNode.Attributes["name"].Value.ToString(), xNode.Attributes["name"].Value.ToString(), xNode.Attributes["email"].Value.ToString(), string.Empty, string.Empty, true, false, DateTime.Parse(xNode.Attributes["created"].Value.ToString()), DateTime.Today, DateTime.Today, DateTime.Today, DateTime.MinValue)); } } } } catch { // Take appropriate action. } // Return the results. return ret; }
Public Overrides Function FindUsersByName( _ ByVal usernameToMatch As String, ByVal pageIndex As Integer, _ ByVal pageSize As Integer, ByRef totalRecords As Integer) _ As System.Web.Security.MembershipUserCollection Dim ret As MembershipUserCollection = Nothing Dim xList As XmlNodeList = Nothing ' Initialize the number of records found. totalRecords = 0 Try ' Get the XML document. xDoc = Helper.GetXmlFile() ' Proceed if it contains something. If xDoc IsNot Nothing Then ' Look for users with matching e-mail addresses. xList = xDoc.SelectNodes("/Users/User[starts-with(@name, '" & _ usernameToMatch & "')]") ' See if there are any matches. If xList IsNot Nothing AndAlso xList.Count > 0 Then 'create a new membershipusercollection ret = New MembershipUserCollection() ' Enumerate each match and add it to the collection. For Each xNode As XmlNode In xList ret.Add(New MembershipUser(Membership.Provider.Name, _ xNode.Attributes("name").Value.ToString(), _ xNode.Attributes("name").Value.ToString(), _ xNode.Attributes("email").Value.ToString(), _ String.Empty, String.Empty, True, False, _ Date.Parse(xNode.Attributes("created").Value.ToString()), _ Date.Today, Date.Today, Date.Today, Date.MinValue)) Next End If End If Catch ex As Exception ' Take appropriate action. End Try ' Return the results. Return ret End Function
Functions Required in a Custom Role Provider
The following sections describe functions (methods) that are required in a custom role provider.
GetRolesForUser Method
The GetRolesForUser function takes a user name as a parameter and returns a string array that contains the names of all groups to which the user belongs.
Important:
|
|---|
|
You must understand one limitation when you are writing code for the GetRolesForUser method. The role provider has a property named System.Web.Security.Roles.CacheRolesInCookie [ Articleimages/f46tewk7(printer).aspx ] . Unfortunately, because of a bug that exists in ASP.NET at the time this article was written, that attribute is not honored, and the role provider's GetRolesForUser method is called every time. In practice, the GetRolesForUser method is called at least once for every user, for every page that he or she visits in the SharePoint site. Because of this, you should implement your own caching mechanism in the GetRolesForUser method in any custom role provider. |
public override string[] GetRolesForUser(string username) { string[] ret = null; XmlNode xNode = null; XmlNodeList xList = null; try { // Get the XML document. xDoc = Helper.GetXmlFile(); // Proceed if it contains something. if (xDoc != null) { // Look for the user. xNode = xDoc.SelectSingleNode("/Users/User[@name='" + username + "']"); // See if it found a match. if (xNode != null) { // Get the collection of group nodes. xList = xNode.ChildNodes[0].ChildNodes; // Resize the array based on number of child nodes. ret = new string[xList.Count]; // Enumerate all the groups in the Groups/Group subnodes // and add to return value. for (int cnt = 0;cnt < xList.Count;cnt++) { ret[cnt] = xList.Item(cnt).InnerText; } } } } catch { // Take appropriate action. } //Return the results. return ret; }
Public Overrides Function GetRolesForUser(ByVal username As String) _ As String() Dim ret As String() = Nothing Dim xNode As XmlNode = Nothing Dim xList As XmlNodeList = Nothing Try ' Get the XML document. xDoc = Helper.GetXmlFile() ' Proceed if it contains something. If xDoc IsNot Nothing Then ' Look for the user. xNode = xDoc.SelectSingleNode("/Users/User[@name='" & _ username & "']") ' See if it found a match. If xNode IsNot Nothing Then ' Get the collection of group nodes. xList = xNode.ChildNodes(0).ChildNodes ' Resize the array based on number of child nodes. ReDim ret(xList.Count - 1) ' Enumerate all the groups in the Groups/Group subnodes and ' add to return value. For cnt As Integer = 0 To xList.Count - 1 ret(cnt) = xList.Item(cnt).InnerText Next End If End If Catch ex As Exception ' Take appropriate action. End Try ' Return the results. Return ret End Function
RoleExists Method
The RoleExists function takes a role name as a parameter. If the role exists, the function returns true; otherwise, it returns false.
public override bool RoleExists(string roleName) { bool ret = false; XmlNode xNode = null; try { // Get the XML document. xDoc = Helper.GetXmlFile(); // Proceed if it contains something. if (xDoc != null) { // Look for the role. xNode = xDoc.SelectSingleNode("/Users/User[Groups/Group='" + roleName + "']"); // Return true if it found a match. if (xNode != null) ret = true; } } catch { // Take appropriate action. } // Return the results. return ret; }
Public Overrides Function RoleExists(ByVal roleName As String) _ As Boolean Dim ret As Boolean = False Dim xNode As XmlNode = Nothing Try ' Get the XML document. xDoc = Helper.GetXmlFile() ' Proceed if it contains something. If xDoc IsNot Nothing Then ' Look for the role. xNode = xDoc.SelectSingleNode("/Users/User[Groups/Group='" & _ roleName & "']") ' Return true if it found a match. If xNode IsNot Nothing Then ret = True End If Catch ex As Exception ' Take appropriate action. End Try ' Return the results. Return ret End Function
Registering the Custom Provider
Now that you understand the minimum required interfaces for a custom Membership and Role provider to work with MOSS 2007 and Windows SharePoint Services 3.0. your next step is to register the custom provider.
To be able to use a custom provider, you must provide a strong name for the assembly it uses, and then register it in the global assembly cache.
To strong name the assembly
-
In Microsoft Visual Studio, right-click the project name, and then click Properties.
-
Click the Signing tab, and then select Sign the assembly.
-
In the Choose a strong name key file list, click <New…>.
-
In the Create Strong Name Key dialog box, type a name for the key file, optionally type a password for the key file, and then click OK.
When you compile the assembly, it builds with the strong name you provided.
You can now add your strong-named assembly to the global assembly cache in several ways, but the most common way is to use the gacutil.exe [ Articleimages/ex0ss12c(printer).aspx ] utility with the /i option. For more information about installing an assembly in the global assembly cache, see How to: Install an Assembly into the Global Assembly Cache [ Articleimages/dkkx7f79(printer).aspx ] .
After you register the assembly, you can extend a Web application into a zone and configure it to use your custom provider (described in the "Setting Up Forms Authentication" section of Forms Authentication in SharePoint Products and Technologies (Part 1): Introduction [ Articleimages/bb975136(printer).aspx ] ).
Following are two different example entries for the custom provider, reflecting that the Microsoft Visual C# and Microsoft Visual Basic assemblies were created with different names. In the examples, when you configure the authentication provider for the zone, the authentication method should be Forms, the membership provider name should be fbaUser, and the role manager name should be fbaRole.
Note:
|
|---|
|
Because the strong name for an assembly that you compile on your computer will differ from the following example, the entries for your custom provider will also differ slightly; specifically the PublicKeyToken value will be different. |
For the Visual C# assembly, use the following code.
<membership defaultProvider="fbaUser"> <providers> <add name="fbaUser" type="Microsoft.IW.customUser, fbaSharp, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=b74d22b2d68547b5" /> </providers> </membership> <roleManager enabled="true" defaultProvider="fbaRole"> <providers> <add name="fbaRole" type="Microsoft.IW.customRole, fbaSharp, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=b74d22b2d68547b5" /> </providers> </roleManager>
<membership defaultProvider="fbaUser"> <providers> <add name="fbaUser" type="Microsoft.IW.customUser, fbaVB, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=ddd85720e6ace42b" /> </providers> </membership> <roleManager enabled="true" defaultProvider="fbaRole"> <providers> <add name="fbaRole" type="Microsoft.IW.customRole, fbaVB, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=ddd85720e6ace42b" /> </providers> </roleManager>
Debugging the Custom Provider
Now that you have compiled the assembly, registered it, and configured a new zone to use the custom provider, you can use and (optionally) debug the provider. You would debug the custom provider in the same way that you would debug any other assembly that is used by another process. In this case, the process that is consuming the assembly is the w3wp.exe process.
To debug the provider, you set the breakpoints you want in the code, and then attach to the w3wp.exe process.
To attach to the w3wp.exe
-
In Visual Studio, on the Tools menu, click Attach to Process to open the Attach to Process dialog box.
-
In Available Processes, select the w3wp.exe process, and then click Select.
-
If you do not see any w3wp processes, select Show processes from all users.
-
Also, if you have recently performed an iisreset, you must navigate to a page in the SharePoint site to create a new w3wp.exe process. Conversely, you might also find multiple w3wp.exe processes running. In such a case, select all w3wp.exe processes. This ensures that you attach to the process that is running your provider assembly.
-
After you attach the w3wp.exe process, try logging on to the site. If you attached a breakpoint on the ValidateUser method of your membership provider, the breakpoint should be hit at the time you log on. If the breakpoint is not hit, you should try reattaching to the w3wp.exe process; another process might have started in connection with logging on to the site. The processes that are already attached appear dimmed (they are disabled); any new w3wp.exe processes appear black and are enabled to allow attachments.
As you log on and navigate to pages, you should see breakpoints being hit in both the membership and role providers. To test your providers further, try using the People Picker to search for users and roles to add to SharePoint groups. This triggers breakpoints set in any of the membership and role provider methods. The number of times the breakpoints are hit varies on what is being done. When you use the People Picker, some methods such as FindUserByEmail can be called more than once. The number of times the role provider is called for any given page can vary also. The role provider is called for each item on the page that requires authentication. Some examples of items are navigation, list view Web Parts, or parts that are targeted at audiences. As a result, the GetRolesForUser method in the role provider is called one or more times for every user on each page view.
Figure 1 and Figure 2 show the People Picker, configured to use the custom membership and role providers. Notice the account name for the entities matches the name attribute of the membership and role provider, as described earlier.
Figure 1. Select People and Groups - membership provider name
Figure 2. Select People and Groups - role provider name
Writing a Custom Forms Logon Page
You may have scenarios that have special logon requirements that cannot be addressed by the default SharePoint forms logon page. For example, you may need to implement a second authentication factor such as Secure ID. Fortunately, because SharePoint Products and Technologies are built on top of ASP.NET 2.0, you can create a custom logon page with your own logon logic, and integrate it directly into MOSS or Windows SharePoint Services.
Creating a Standard ASP.NET Web Site
The easiest way to create a custom logon page is to build a standard ASP.NET Web application. This enables you to create a site and easily debug the code behind for your forms logon page to ensure that it is working correctly. When you do this, you should configure your site to use the same membership and role provider that you intend to use with MOSS or Windows SharePoint Services.
In the scenario for this article, we built a custom forms logon page for two reasons:
-
To show a custom policy condition that all site users must agree to before using the site.
-
To force certain users to use two-factor authentication, where the second factor is Secure ID.
We have created an ASPX page named customLogin.aspx and added it to the site. It contains the following:
-
Edit boxes for user name, password, and Secure ID number
-
A check box for a persistent authorization cookie
-
A button to perform the logon process
Figure 3 shows how the page should look.
Figure 3. customLogin.aspx page
The web.config file was modified to do the following:
-
Use forms authentication and the fbaVB membership and role provider described earlier in this article, in the section Registering the Custom Provider.
-
Deny access to anonymous users.
Default.aspx was also added to the site when the project was created, and it is used in the site for testing.
Adding Code Behind the Logon Page
In the code behind the logon page, you should first validate the credentials. You can use the ValidateUser method of the Membership class to do this. If the credentials are valid, you can redirect the user to the site; otherwise, prompt them to reenter their credentials.
if (Membership.ValidateUser(UserTxt.Text, PwdTxt.Text)) { // Do Secure ID thing here. // Redirect the user to the requested page. FormsAuthentication.RedirectFromLoginPage(UserTxt.Text, SaveChk.Checked); } else StatusLbl.Text = "The credentials you entered are not valid. " + "Please try again.";
If Membership.ValidateUser(UserTxt.Text, PwdTxt.Text) Then ' Do Secure ID thing here. ' Redirect the user to the requested page. FormsAuthentication.RedirectFromLoginPage(UserTxt.Text, _ SaveChk.Checked) Else StatusLbl.Text = "The credentials you entered are not valid. " & _ Please try again." End If
For the Secure ID component, this example simulates only the process that a user or another system that requires additional authentication processing would go through. In this example, the page is hard-coded to prompt a specific user for a Secure ID number; in practice you could query a database or Web service (or whatever is appropriate) to determine how to process users. After prompting the user for the Secure ID credentials, it allows them to pass.
// Instead of redirecting user3, ask for a Secure ID number. if (UserTxt.Text.ToLower() == "user3") { // Update UI. StatusLbl.Text = "Please enter your Secure ID number"; LogPnl.Visible = false; SecurePnl.Visible = true; }
' Instead of redirecting user3, ask for a Secure ID number. If UserTxt.Text.ToLower() = "user3" Then ' Update UI. StatusLbl.Text = "Please enter your Secure ID number" LogPnl.Visible = False SecurePnl.Visible = True End If
Compiling the Application into an Assembly
After you complete the simple ASP.NET application and have the forms logon page working correctly, your next step is to compile it and use it in MOSS or Windows SharePoint Services. You can use the Web site project that is new with Visual Studio 2005 to create the Web application. Alternatively, with Service Pack 1 of Visual Studio you can also create the Web project model that is similar to Visual Studio 2003, in that you can compile the project into a single assembly (.dll file).
Note:
|
|---|
|
It is often easier to debug integrated solutions such as this if you use the latter approach and compile the Web application into a single DLL. |
For the remainder of this section, we assume that you have compiled the project into a single assembly. If you use the Visual Studio 2005 style of Web site project, you can still make the application work, but you must use the ASP.NET Compilation Tool (Aspnet_compiler.exe) [ Articleimages/ms229863(printer).aspx ] to precompile it. If you use that approach, ensure that you include the -u parameter when you compile so that you can update the assembly later.
Copying Files and Registering the Assembly
Following are the steps to make the custom logon page available to MOSS or Windows SharePoint Services.
To enable the use of the custom logon page by SharePoint Products and Technologies
-
Copy the logon page to the _layouts directory. The default path is C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS.
-
Create a bin directory on the file system at the top-level (root) of your SharePoint Web application that will use your custom logon page. For the examples used in this article, the default directory would be in the path C:\Inetpub\wwwroot\wss\VirtualDirectories\ www.contoso.com80. Then, copy the compiled assembly for the custom forms application to the bin directory that you created.
-
Register the compiled assembly for the custom forms application in the global assembly cache. You can do this by using the Global Assembly Cache Tool (Gacutil.exe) [ Articleimages/ex0ss12c(printer).aspx ] with the /i option. For more details about installing an assembly in the global assembly cache, see How to: Install an Assembly into the Global Assembly Cache [ Articleimages/dkkx7f79(printer).aspx ] .
You must also update the web.config file for the Web application that is going to use the custom logon page.
To update the web.config file for the Web application
-
Open the web.config file.
-
In the authentication section, locate the forms element.
-
In the forms element, change the loginUrl attribute to point to your new custom forms logon page; for example, _layouts/customLogin.aspx.
-
Perform an iisreset.
Note:
This step is important; things might not work correctly if you do not perform an iisreset.
You should now be able to use the custom logon page.
Following is an example of what the page looks like when used with the Contoso site described earlier in this article.
Figure 4. customLogin.aspx page on Contoso site
The interface is obviously not quite what one would expect from a SharePoint site. Fortunately, you can copy elements from the login.aspx page that is included with SharePoint Products and Technologies and paste them into the custom logon page to give it the SharePoint appearance.
To modify the custom logon page to give it the SharePoint appearance
-
In the @ Page directive at the top of the page, add the following master page attribute.
-
Remove the following lines from near the top of the page.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html > <head id="Head1" runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server">
-
Add the following lines directly below the @ Page directive.
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %> <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,
PublicKeyToken=71e9bce111e9429c" %> <%@ Import Namespace="Microsoft.SharePoint" %> <asp:Content ContentPlaceHolderId="PlaceHolderPageTitle" runat="server"> <SharePoint:EncodedLiteral runat="server"
text="<%$Resources:wss,login_pagetitle%>"
EncodeMethod='HtmlEncode'/> </asp:Content> <asp:Content ContentPlaceHolderId="PlaceHolderTitleBreadcrumb" runat="server"> </asp:Content> <asp:Content ContentPlaceHolderId="PlaceHolderPageTitleInTitleArea" runat="server"> <SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,login_pagetitle%>"
EncodeMethod='HtmlEncode'/> </asp:Content> <asp:Content ContentPlaceHolderId="PlaceHolderSiteName" runat="server"/> <asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">
-
Remove the following lines from the bottom of the page.
</form> </body> </html>
-
Add the following line to the bottom of the page.
</asp:Content>
Try logging on to the site again; you may need to refresh the page. After you refresh, the page should look like Figure 5 and Figure 6.
Figure 5. Updated customLogin.aspx on Contoso site - enter user name and password
Figure 6. Updated customLogin.aspx on Contoso site - enter Secure ID
Using Web Services with a Site Protected by Forms Authentication
Using the SharePoint Web services with a site secured with forms authentication works; however, the process differs from what you would do to use Web services for a site that is secured with Windows authentication. The primary difference is that you must obtain an authentication cookie and then use that cookie when accessing the Web services in the site protected by forms authentication.
Fortunately, SharePoint Products and Technologies provide a new Web service that makes it easier to work in this scenario: the Authentication Web service [ Articleimages/aa979750(printer).aspx ] . It has a method named Login [ Articleimages/aa979746(printer).aspx ] that, when called, places an authentication cookie in the proxy's System.Web.Services.Protocols.HttpWebClientProtocol.CookieContainer [ Articleimages/yta0fyx9(printer).aspx ] collection. That cookie can then be used in subsequent requests to other Web services in the site that is protected by forms authentication to authenticate the request.
Note:
|
|---|
|
By default, a zone that is configured to use forms authentication does not enable Client Integration features. This option, which is found on the Authentication Provider page in SharePoint Central Administration, must be turned on if you want to use the SharePoint Web services. When this option is turned off, SharePoint Products and Technologies also turn off support for remote interfaces, such as Web services. |
For purposes of creating your Web service proxies (such as adding Web references in Visual Studio [ Articleimages/8dcbc50t(printer).aspx ] ), use a Windows authentication–protected site. In most cases, the Visual Studio Add Web Reference wizard does not work with a SharePoint site that is protected by forms authentication. For this example, we retrieve all of the lists in the site by using the Lists Web service [ Articleimages/ms774654(printer).aspx ] .
To retrieve all lists in the site by using the Lists Web services
-
Start Visual Studio, and create a Windows Application project.
-
Add a button and text box to Form1.
-
Change the text box properties so that Multiline is True and Scrollbars is Vertical.
-
Resize the text box to fill the form under the button.
-
Add a Web reference to the Authentication Web service in the site that is protected by forms authentication; the Authentication Web service can be found in the path http://siteCollectionName/_vti_bin/authentication.asmx. Name this Web reference fbaAuth.
-
Add a second Web reference to the Lists Web service; it can be found in the path http://siteCollectionName/_vti_bin/lists.asmx. Name this Web reference fbaLists.
-
On the form, double-click Button1 to switch to code view and create an event handler for the click event. Create two variables for the Web service proxies, as shown in the following code.
fbaAuth.Authentication auth = new fbaAuth.Authentication(); fbaLists.Lists lists = new fbaLists.Lists();
Dim auth As New fbaAuth.Authentication() Dim lists As New fbaLists.Lists()
-
Create a CookieContainer collection object for the Authentication class proxy; the authentication cookie will be stored in this container after calling the Login method.
-
Call the Login method, and check the result from that call, as shown in the following code.
auth.CookieContainer = new System.Net.CookieContainer(); auth.AllowAutoRedirect = true; fbaAuth.LoginResult lr = auth.Login("myUserName", "myUserPassword"); if (lr.ErrorCode == fbaAuth.LoginErrorCode.NoError) { //Now we can talk to the Lists Web service. }
auth.CookieContainer = New System.Net.CookieContainer() auth.AllowAutoRedirect = True Dim lr As fbaAuth.LoginResult = auth.Login("myUserName", _ "myUserPassword") If lr.ErrorCode = fbaAuth.LoginErrorCode.NoError Then 'Now we can talk to the Lists Web service. End If
If the Login method succeeds, the proxy for the Authentication Web service has a valid authentication cookie in its CookieContainer collection. To reuse this cookie, just set the CookieContainer property for the Lists Web service proxy equal to the CookieContainer property of the Authentication Web service proxy. You can then make calls to the Lists Web service, and the authentication cookie will be used to authenticate and authorize the request. That means that when you make a Web service request, the cookie with authenticate and authorize the request based on the permissions of whatever account was used to create the authentication cookie.
Following is the remainder of the code sample.
if (lr.ErrorCode == fbaAuth.LoginErrorCode.NoError) { // Now we can talk to the Lists Web service. lists.CookieContainer = auth.CookieContainer; XmlNode xData = lists.GetListCollection(); }
If lr.ErrorCode = fbaAuth.LoginErrorCode.NoError Then ' Now we can talk to the Lists Web service. lists.CookieContainer = auth.CookieContainer Dim xData As XmlNode = lists.GetListCollection() End If

