Archive

Author Archive

SharePoint: How to create a custom REST WCF Service available in _layouts folder

August 8th, 2013 No comments

    In the given blogpost I’ll describe step by step how to create a simple REST WCF Service residing in the _layouts folder. To create a demonstration project we’ll use Visual Studio 2012. I believe, however, the similar steps could be done in Visual Studio 2010 as well.

* Don’t look for a meaning in the source code of the demonstration project as its main goal is just to show how to communicate with WCF Services operating under SharePoint and how they return and accept parameters of standard and custom types.

* If you are too lazy to go through all steps, you can download the demonstration project right away and play with it πŸ™‚

1. Create a SharePoint project in the Visual Studio 2012

* Note that if you plan to add the service to an existent project, just skip this step.

Click to see how to create the SharePoint project in details

  1. Go to FILE -> New -> Project and select the SharePoint 2010 Project available under Templates/Visial C#/SharePoint (see the image below). Type the name and location of the project. Let’s name it CustomRestService.
    Create SharePoint Project In Visual Studio 2012
  2. Click OK button, then the SharePoint Customization Wizard opens (see the image below). Type the proper URL of the local site for debugging and choose the Deploy as a farm solution option.
    SharePoint Customization Wizard
  3. Click Finish button. In Solution Explorer the created project resembles the following:
    SharePoint Project After Creation


2. Add a WCF Service to the project

Click to see how to add WCF Service to the project in details

  1. Ensure that the CustomRestService project is selected in Solution Explorer and then go to PROJECT -> Add New Item. In the opened dialog, locate the WCF Service among available templates. It’s usually under the Visual C# Items. Alternatively you can find the required template by typing WCF in the search box (Search Installed Templates) at the right top corner (see the picture below). Let’s name the service CustomRestService as well.
    Add WCF Service to the project
  2. Click Add button. After the WCF Service has been added, the project should look like the following:
    SharePoint Project After WCF Service has been added
    There are 3 new files in the project:

    • ICustomRestService.cs, defines the interface supported by the service, i.e. specifies the methods available in the service;
    • CustomRestService.cs, provides the actual implementation of the service’s methods;
    • app.config, presets the WCF Service configuration;

    Delete the app.config from the project as we don’t actually need it.
    * If you don’t have the WCF Service template available in Visual Studio or you don’t want to use it for some reason, you can just add two .cs files, ICustomRestService.cs and CustomRestService.cs, to the project. In this case don’t forget to add references to such assemblies as System.Runtime.Serialization and System.ServiceModel.


3. Add references to System.ServiceModel.Web and System.Web assemblies to the project

Click to see how to add references to the project in details

  1. Ensure the CustomRestService project is selected in Solution Explorer and then go to PROJECT -> Add Reference…. In the opened dialog, find the System.ServiceModel.Web and System.Web assemblies (usually they are located under Assemblies/Framework) and check the checkboxs against them (see the picture below).
    Add References to System.ServiceModel.Web and System.Web to the project
  2. Click OK button. The References at that stage for the project created from scratch should look like the following:
    References After Adding System.ServiceModel.Web and System.Web Assemblies
    The highlighted assemblies are essential for developing WCF Services operating under SharePoint.


4. Modify the service’s interface

Open the ICustomRestService.cs and bring it to the following state:

using System.ServiceModel;
using System.ServiceModel.Web;

namespace CustomRestService
{
    /// <summary>
    /// Wraps the service result and provides the error message (if any)
    /// </summary>
    public class ServiceResult<T>
    {
        public bool   Success      { get; set; }
        public string ErrorMessage { get; set; }
        public T      Data         { get; set; }
    }

    /// <summary>
    /// Represents basic info about a book
    /// </summary>
    public class Book
    {
        public int    Id     { get; set; }
        public string Title  { get; set; }
        public string Author { get; set; }
    }

    /// <summary>
    /// Service's interface
    /// </summary>
    [ServiceContract]
    public interface ICustomRestService
    {
        /// <summary>
        /// Checks if book is already registered in the system
        /// </summary>
        [OperationContract]
        [WebInvoke(Method  = "POST", 
            RequestFormat  = WebMessageFormat.Json, 
            ResponseFormat = WebMessageFormat.Json, 
            BodyStyle      = WebMessageBodyStyle.WrappedRequest)]
        ServiceResult<bool> BookExists(string bookTitle);

        /// <summary>
        /// Returns book by id
        /// </summary>
        [OperationContract]
        [WebInvoke(Method  = "POST", 
            RequestFormat  = WebMessageFormat.Json, 
            ResponseFormat = WebMessageFormat.Json, 
            BodyStyle      = WebMessageBodyStyle.WrappedRequest)]
        ServiceResult<Book> GetBook(int id);

        /// <summary>
        /// Adds the passed book to the system
        /// </summary>
        [OperationContract]
        [WebInvoke(Method  = "POST",
            RequestFormat  = WebMessageFormat.Json,
            ResponseFormat = WebMessageFormat.Json,
            BodyStyle      = WebMessageBodyStyle.WrappedRequest)]
        ServiceResult<int> AddBook(Book book);
    }
}

Here the ServiceResult is a simple class that wraps the service result and provides the error message in case it occurs. The Book class provides basic info about a book and is intended to show how REST WCF Service deals with custom types.

5. Modify the service implementation

Open the CustomRestService.cs and modify it so that it looks like the following:

using System;
using System.ServiceModel.Activation;
using System.Web;
using Microsoft.SharePoint;

namespace CustomRestService
{
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class CustomRestService : ICustomRestService
    {
        public ServiceResult<bool> BookExists(string bookTitle)
        {
            // allow access for anonymous users

            if (string.IsNullOrEmpty(bookTitle))
                return new ServiceResult<bool>()
                { 
                    Success      = false, 
                    ErrorMessage = "Book title is undefined!" 
                };

            var result = new ServiceResult<bool>() { Success = true };

            try
            {
                SPList spList = SPContext.Current.Web.Lists["Books"];

                SPQuery spQuery = new SPQuery
                {
                    Query = "<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>" + 
                        bookTitle.ToLower() + "</Value></Eq></Where>",
                    ViewAttributes = "Scope='Recursive'"
                };

                SPListItemCollection books = spList.GetItems(spQuery);
                result.Data = books.Count > 0;
            }
            catch (Exception ex)
            {
                result.Success      = false;
                result.ErrorMessage = ex.Message;
            }

            return result;
        }

        public ServiceResult<Book> GetBook(int id)
        {
            // check if user is authenticated
            if (!HttpContext.Current.Request.IsAuthenticated)
                return new ServiceResult<Book>()
                    {
                        Success      = false, 
                        ErrorMessage = "Unauthenticated access!"
                    };

            if (id <= 0)
                return new ServiceResult<Book>()
                    {
                        Success      = false, 
                        ErrorMessage = "Invalid book id!"
                    };

            var result = new ServiceResult<Book>() { Success = true };

            try
            {
                SPList spList = SPContext.Current.Web.Lists["Books"];

                SPListItem spListItem = spList.GetItemById(id);
                result.Data = new Book()
                {
                    Id     = spListItem.ID, 
                    Title  = spListItem.Title, 
                    Author = Convert.ToString(spListItem["BookAuthor"])
                };
            }
            catch (Exception ex)
            {
                result.Success      = false;
                result.ErrorMessage = ex.Message;
            }

            return result;
        }

        public ServiceResult<int> AddBook(Book book)
        {
            // check if user is authenticated
            if (!HttpContext.Current.Request.IsAuthenticated)
                return new ServiceResult<int>()
                    {
                        Success      = false, 
                        ErrorMessage = "Unauthenticated access!"
                    };

            if (book == null)
                return new ServiceResult<int>()
                    {
                        Success      = false, 
                        ErrorMessage = "Invalid book!"
                    };

            // checking if the book is already presented is omitted

            var result = new ServiceResult<int>() { Success = true };

            try
            {
                SPList spList = SPContext.Current.Web.Lists["Books"];

                SPListItem spListItem = spList.AddItem();
                spListItem["Title"]      = book.Title;
                spListItem["BookAuthor"] = book.Author;

                SPContext.Current.Web.AllowUnsafeUpdates = true;
                spListItem.Update();
                SPContext.Current.Web.AllowUnsafeUpdates = false;

                result.Data = spListItem.ID;
            }
            catch (Exception ex)
            {
                result.Success      = false;
                result.ErrorMessage = ex.Message;
            }

            return result;                
        }
    }
}

The implementation operates with the Books list and assumes that the list is available at the SharePoint Site in the boundaries of which the service is called. Note that we work with SharePoint objects in the same way as we would do this in code-behind of an .aspx-page.

The GetBook and AddBook methods require users to be authenticated before calling them, while the BookExists allows anonymous access (of course, in case it’s turned on for Web Application and Site). Since it’s a best practice, always put the checking whether user is authenticated and authorized in the beginning of every web method unless you have some contrary requirements (for example, to provide anonymous access).

6. Add Layouts mapped folder to the project

* Note that if you already have the folder, just skip this step.

Click to see how to add “Layouts” mapped folder to the project in details

    Ensure that the CustomRestService project is selected in Solution Explorer and then go to PROJECT -> Add SharePoint “Layouts” Mapped Folder. The Layouts folder along with the nested CustomRestService folder will be added to the project. For simplicity rename the nested CustomRestService to Wcf. After that the project should look as follows:
The project after the Layouts mapped folder has been added


7. Add .svc file to the Layouts folder

Click to see how to add .svc file to the Layouts folder in details

  1. Ensure that the Wcf folder nested in the Layouts is selected in Solution Explorer and then go to PROJECT -> Add New Item. In the opened dialog, locate the Text File among available templates. It’s usually under the Visual C# Items/General (see the image below). Name the file CustomRestService.svc.
    Add .svc file to the project
  2. Click Add button. After the .svc file has been added, the project should look like the following:
    The project after the .svc file has been added


8. Modify the .svc file

Open the CustomRestService.svc and bring it to the following:

<%@ ServiceHost Language="C#" Debug="true" 
    Service="CustomRestService.CustomRestService, $SharePoint.Project.AssemblyFullName$"
    CodeBehind="CustomRestService.cs"
    Factory="Microsoft.SharePoint.Client.Services.MultipleBaseAddressWebServiceHostFactory,
             Microsoft.SharePoint.Client.ServerRuntime, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

The MultipleBaseAddressWebServiceHostFactory declares the REST type of the Web Service and creates proper endpoints with Web Bindings. The factory takes upon itself the configuring of the Web Service, that is why we don’t need the app.config deleted previously.

The Service attribute provides the Fully Qualified Name (including the assembly’s full name) of the class representing the service. Instead of putting in the proper full name of the assembly I used such replaceable parameter as $SharePoint.Project.AssemblyFullName$. When building the project, Visual Studio is supposed to replace the parameter with the full name of the output assembly. One nuisance here is that Visual Studio doesn’t perform replacement in .svc files by default. To make Visual Studio look into .svc, we have to manually alter the .csproj. So, open the project’s .csproj file in Notepad or other Notepad-like text editor, find the first <PropertyGroup> node, insert the following before the closing </PropertyGroup> and save the file:

<TokenReplacementFileExtensions>svc</TokenReplacementFileExtensions>

* Note that, having modified the .csproj, you likely will be prompted by Visual Studio to reload the project.

After the alteration of the project’s file it should resemble the following (some tags and attributes are skipped for simplicity):

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" ...>
  <Import ... />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{16585D01-2995-48EF-8DFA-1371C9EF6EE9}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>CustomRestService</RootNamespace>
    <AssemblyName>CustomRestService</AssemblyName>
    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{BB1F664B-9266-4fd6-B973-E1E44974B511};...</ProjectTypeGuids>
    <SandboxedSolution>False</SandboxedSolution>
    <WcfConfigValidationEnabled>True</WcfConfigValidationEnabled>
	<TokenReplacementFileExtensions>svc</TokenReplacementFileExtensions>
  </PropertyGroup>
...
</Project>

* Note that for the project attached to the given article, the .csproj has been already modified.

Of course, instead of the $SharePoint.Project.AssemblyFullName$ you can use the real full name of your assembly. To get such name I usually use .Net Reflector. Alternatively you can make Visual Studio display the name, following instructions from the article How to: Create a Tool to Get the Full Name of an Assembly.

9. Build the project and deploy it

Right-click the CustomRestService project in Solution Explorer and then click Deploy. Behind the scenes, Visual Studio compiles the project, creates a .wsp file and deploys the package to the farm.

10. Test the service

  1. First of all try to open the service in a browser. You can use for that any SharePoint Site, just add the /_layouts/wcf/customrestservice.svc at the end of the URL. For example:
    http://someservername:1200/somesite/_layouts/wcf/customrestservice.svc
    

    If you see the message “Endpoint not found“, don’t worry, that’s what we need. That means all of the steps above have been done correctly.

  2. Let’s test the web methods of the service. As you remember, the service interacts with the Books list. It’s a list with two main fields: Title and BookAuthor, both are Single Line of Text (see the image below).
    Books SharePoint List
    * I do not cite the list’s schema here as you can find it (schema, instance, feature to create the instance) in the attached project.
    The test page named TestPage.aspx is added to the demonstration project as well. The page has been created as an Application Page, locates in the Layouts folder and contains one button that executes JavaScript functions to test each web method. Below is the shortened listing of the page to show how to call web methods using jQuery and Ajax:

    ...
    <%@ Page ... CodeBehind="TestPage.aspx.cs" 
             Inherits="CustomRestService.Layouts.TestPage" 
    		 DynamicMasterPageFile="~masterurl/default.master" %>
    
    <asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
     <script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
    
     <script type="text/javascript">
      // makes POST json-request to a web method
      var getJsonData = function (url, param, callback, errorcallback) {
        $.ajax({
    	  url        : url,
    	  dataType   : "json",
    	  type       : "POST",
    	  contentType: 'application/json; charset=utf-8',
    	  data       : JSON.stringify(param),
    	  success    : function (data) {
    		  if (callback)
    			  callback(data);
    	  },
    	  error      : function (XMLHttpRequest, textStatus, errorThrown) {
    		  if (errorcallback)
    			  errorcallback(XMLHttpRequest, textStatus, errorThrown);
    	  }
        });
      };
    
      // makes request to the BookExists method
      function testBookExists() {
        getJsonData('Wcf/CustomRestService.svc/BookExists', 
          { "bookTitle": 'the adventures of tom sawyer' },
      	  function (data) {
    		  if (data.Success)
    			  alert('Success. Result: ' + data.Data);
    		  else
    			  alert('Fail. Error: ' + data.ErrorMessage);
    	  },
    	  function(XMLHttpRequest, textStatus, errorThrown) {
    		  alert(errorThrown);
    	  });	
      }
    
      // makes request to the GetBook method
      function testGetBook() {
        getJsonData('Wcf/CustomRestService.svc/GetBook', { "id": 2 },
    	  function (data) {
    		  if (data.Success)
    			  alert('Success. Title: ' + data.Data.Title + ' Author: ' + data.Data.Author);
    		  else
    			  alert('Fail. Error: ' + data.ErrorMessage);
    	  },
    	  function (XMLHttpRequest, textStatus, errorThrown) {
    		  alert(errorThrown);
    	  });
      }
    
      // makes request to the AddBook method
      function testAddBook() {
        getJsonData('Wcf/CustomRestService.svc/AddBook', 
          { "book": { "Author": "Mark Twain", "Title": "The Adventures of Huckleberry Finn" } },
    	  function (data) {
    		  if (data.Success)
    			  alert('Success. Book Id: ' + data.Data);
    		  else
    			  alert('Fail. Error: ' + data.ErrorMessage);
    	  },
    	  function (XMLHttpRequest, textStatus, errorThrown) {
    		  alert(errorThrown);
    	  });
      }
     </script>
    </asp:Content>
    
    <asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
     <asp:Button ID="test" Text="Test Web Methods" runat="server" 
       OnClientClick="testBookExists(); testGetBook(); testAddBook(); return false;" />
    </asp:Content>
    ...
    

    Pay attention to the URLs used to send request to web methods. The given test page is located directly in _layouts, so to access the Web Service placed in the _layouts/Wcf I use the relative URL Wcf/CustomRestService.svc. If your page dealing with the Web Service resides somewhere outside of _layouts or within the folders nested in _layouts you have to provide the proper URL (relative or not) to the Web Service.
    Open the test page in a browser, the URL should be like the following

    http://someservername:1200/somesite/_layouts/TestPage.aspx
    

    Click Test Web Methods button. If everything works fine you’ll get 3 alert messages and each of them starts with the word “Success”. The order of the alerts may vary as requests are asynchronous.

The demonstration project you can download here – CustomRestService.zip. Note that the archive contains the .wsp package in the CustomRestService\bin subfolder, use it in case you are not able to compile/build the project for some reason.

SharePoint: How to add Links and Headings to Quick Launch programmatically

July 25th, 2013 No comments

    The Quick Launch is a collection of links usually available on every page in the left sidebar (of course, in case you use a standard master page and don’t open the page within a modal dialog), see the image below:

Add Heading to Quick Launch

The links are pretty easily manageable through the UI using Site Settings -> Quick Launch. There Headings and links can be added, removed or reordered. Besides UI, manipulations of the Quick Launch can be done through the SharePoint Object Model. For example, to add a link to the Quick Launch I usually use the following method:

public static SPNavigationNode AddLinkToQuickLaunch(SPNavigationNode parentNode, 
                                        string title, string url, bool isExternal)
{
	SPNavigationNode node = new SPNavigationNode(title, url, isExternal);
	parentNode.Children.AddAsLast(node);

	// refresh navigation node just in case
	node = parentNode.Navigation.GetNodeById(node.Id);
	return node;
}

The parentNode here represents either an existent link or the root SPWeb.Navigation.QuickLaunch object.

Unlike UI where the New Heading option is presented (see the image above), SharePoint Object Model doesn’t provide any special method or node class that would be intended to add a Heading to the Quick Launch. However, we still can turn a usual SPNavigationNode into Heading. For that we have to set some properties available through the SPNavigationNode.Properties. The listing below demonstrates two methods allowing to add a Heading:

public static SPNavigationNode AddHeadingToQuickLaunch(SPWeb spWeb, string headingName)
{
	SPNavigationNodeCollection quicklaunchNav = spWeb.Navigation.QuickLaunch;

	SPNavigationNode headingNode = 
                      new SPNavigationNode(headingName, "javascript:window.goback(0)", true);
	headingNode = quicklaunchNav.AddAsLast(headingNode);

	//turn the node into Heading
	TurnIntoHeading(headingNode);

	headingNode.Update();

	// refresh navigation node just in case
	headingNode = spWeb.Navigation.GetNodeById(headingNode.Id);
	return headingNode;
}
public static void TurnIntoHeading(SPNavigationNode node)
{
	node.Properties["NodeType"]             = "Heading";
	node.Properties["BlankUrl"]             = "True";
	
	node.Properties["LastModifiedDate"]     = DateTime.Now;
	node.Properties["Target"]               = "";
	node.Properties["vti_navsequencechild"] = "true";
	node.Properties["UrlQueryString"]       = "";
	node.Properties["CreatedDate"]          = DateTime.Now;
	node.Properties["Description"]          = "";
	node.Properties["UrlFragment"]          = "";
	node.Properties["Audience"]             = "";
}

Note that the given implementation makes the Heading void, i.e. clicking the Heading neither leads to another page nor refreshes the current one. That’s possible due to the javascript:window.goback(0) that is passed as a URL of the node. I consider Heading as a name of the group of subjacent links, therefore I prefer having void links for Headings.

SharePoint: SqlMembershipProvider – Lock User

July 21st, 2013 No comments

    In addition to the article SharePoint: SqlMembershipProvider – Get All Users In Role, here is one more method to extend the SqlMembershipProvider with. It’s found out that the SqlMembershipProvider doesn’t provide a method to lock user. By default a user can be automatically locked after several frequent and failed attempts to login. To unlock such users the SqlMembershipProvider supplies with the UnlockUser method. But what if administrator wants to temporarily lock user for some reason? Unfortunately, there is no such method out-of-box.

So, let’s try to implement our own LockUser method. Two obvious steps for that are as follows: to create a Stored Procedure in database; to extend a class derived from the SqlMembershipProvider with the proper method.

LockUser Stored Procedure

The stored procedure is very simple as we need just to update one field in the aspnet_Membership table for appropriate user. Below is the script to create such procedure. Run the script on MembershipProvider database, in my case it’s aspnetdb.

USE [aspnetdb]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

-- =============================================
-- Author:      .Net Follower
-- Description:	Locks User
-- =============================================
CREATE PROCEDURE [dbo].[aspnet_Membership_LockUser]
	@ApplicationName                         nvarchar(256),
    @UserName                                nvarchar(256)
AS
BEGIN
	DECLARE @UserId uniqueidentifier
    SELECT  @UserId = NULL
    SELECT  @UserId = u.UserId
    FROM    dbo.aspnet_Users u, dbo.aspnet_Applications a, dbo.aspnet_Membership m
    WHERE   LoweredUserName = LOWER(@UserName) AND
            u.ApplicationId = a.ApplicationId  AND
            LOWER(@ApplicationName) = a.LoweredApplicationName AND
            u.UserId = m.UserId

    IF ( @UserId IS NULL )
        RETURN 1

    UPDATE dbo.aspnet_Membership 
    SET IsLockedOut = 1 WHERE @UserId = UserId

    RETURN 0
END

Custom Membership Provider

Now we can add the LockUser method to the custom Membership Provider called SqlMembershipProviderEx and shown in the article. The SqlMembershipProviderEx with the LockUser is listed below. Note that the methods mentioned in the previous article are skipped.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Globalization;
using System.Reflection;
using System.Web.Security;
using Microsoft.SharePoint;

namespace dotNetFollower
{
    public class SqlMembershipProviderEx : SqlMembershipProvider
    {       
	    ...
		
        public bool LockUser(string username)
        {
            bool flag = false;
            CheckParameter(ref username, true, true, true, 0x100, "username");

            DoInSqlConnectionContext(delegate(SqlConnection connection)
            {
                //this.CheckSchemaVersion(connection.Connection);
                SqlCommand command = new SqlCommand("dbo.aspnet_Membership_LockUser", connection)
                {
                    CommandTimeout = CommandTimeout,
                    CommandType    = CommandType.StoredProcedure
                };
                command.Parameters.Add(CreateInputParam("@ApplicationName", SqlDbType.NVarChar, ApplicationName));
                command.Parameters.Add(CreateInputParam("@UserName", SqlDbType.NVarChar, username));
                SqlParameter parameter = new SqlParameter("@ReturnValue", SqlDbType.Int)
                {
                    Direction = ParameterDirection.ReturnValue
                };
                command.Parameters.Add(parameter);

                command.ExecuteNonQuery();
                flag = ((parameter.Value != null) ? ((int)parameter.Value) : -1) == 0;
            });

            return flag;
        }

        protected internal static void CheckParameter(ref string param, bool checkForNull, bool checkIfEmpty, bool checkForCommas, int maxSize, string paramName)
        {
            if (param == null)
            {
                if (checkForNull)
                    throw new ArgumentNullException(paramName);
            }
            else
            {
                param = param.Trim();
                if (checkIfEmpty && (param.Length < 1))
                    throw new ArgumentException(string.Format("The parameter '{0}' must not be empty.", new object[] { paramName }), paramName);

                if ((maxSize > 0) && (param.Length > maxSize))
                    throw new ArgumentException(string.Format("The parameter '{0}' is too long: it must not exceed {1} chars in length.", new object[] { paramName, maxSize.ToString(CultureInfo.InvariantCulture) }), paramName);

                if (checkForCommas && param.Contains(","))
                    throw new ArgumentException(string.Format("The parameter '{0}' must not contain commas.", new object[] { paramName }), paramName);
            }
        }

        ...
        
    }    
}

The latest version of the SqlMembershipProviderEx along with all used additional classes are available to download here.

Related posts:

SharePoint: HttpContext.Current is null in event receivers

July 12th, 2013 No comments

    I have never used HttpContext in event receivers till recently, so I was quite surprised when I got a NullReferenceException, trying to access HttpContext.Current.Request within ItemAdding. I would never play with the HttpContext.Current inside such methods as ItemAdded, ItemUpdated and so on as they are usually asynchronous and might be executed on any machine of SharePoint farm. But why the HttpContext.Current is null within synchronous ItemAdding, ItemUpdating, etc. it’s a riddle for me. On the other hand, within the constructor of SPItemEventReceiver the HttpContext.Current is valid. So, the possible workaround here is to get current HttpContext inside the constructor, save it in a variable and then use in synchronous methods. In my opinion the best way in this case is to have a class that is derived from SPItemEventReceiver, manipulates HttpContext and serves as a base class for all custom event receivers. Such simple class could resemble the following:

public class MyAppSPItemEventReceiverBase : SPItemEventReceiver
{
	protected readonly HttpContext _currentContext = null;

	public MyAppSPItemEventReceiverBase()
	{
		_currentContext = HttpContext.Current;
	}
}

Every custom event receiver in that case should look like the following:

public class SomeCustomEventReceiver : MyAppSPItemEventReceiverBase
{
	public override void ItemUpdating(SPItemEventProperties properties)
	{
		base.ItemUpdating(properties);

		properties.AfterProperties["UpdatedFrom"] = GetIpAddress(_currentContext);
	}

	protected static string GetIpAddress(HttpContext context)
	{
		string ipAddress = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
		if (string.IsNullOrEmpty(ipAddress))
			return context.Request.ServerVariables["REMOTE_ADDR"];
		string[] tmpArray = ipAddress.Split(',');
		return tmpArray[0];
	}
}

SharePoint: What is a People Picker? Part 2 – Picker.aspx and PeoplePickerDialog

July 2nd, 2013 No comments

In the previous article about People Picker functionality I described the PeopleEditor control. The Browse button of the PeopleEditor initiates the search dialog containing the Picker.aspx page described below.

Picker.aspx

Picker.aspx is a page used for many built-in entity pickers and, along with its master-page pickerdialog.master, located at 14\TEMPLATE\LAYOUTS. Both the page and master-page contain a lot of PlaceHolders that are stuffed with different controls and contents depending on the type of the picker we use. In case of the People Picker the most noteworthy PlaceHolders defined in the Picker.aspx are

  • PlaceHolderDialogControl;
  • PlaceHolderQueryControl;
  • PlaceHolderHtmlMessage;
  • PlaceHolderError;
  • PlaceHolderResultControl;
  • PlaceHolderEditorControl.

Take a look at a simplified markup below to figure out where these PlaceHolders are situated within the Picker.aspx (the Html comments, indents and formatting are added for clarity):

Click to open the Picker.aspx’s markup with PlaceHolders

...
<asp:Content contentplaceholderid="PlaceHolderDialogBodySection" 
    runat="server">
 ...
 <!-- PlaceHolderDialogControl -->
 <asp:PlaceHolder runat="server" id="PlaceHolderDialogControl"/>
	
  <table class="ms-pickerbodysection" cellspacing="0" cellpadding="0" 
          width='100%' height='100%'>
   ...
   <tr>
    <td width='15px'>&#160;</td>
    <td width='100%' height="20px">
     <!-- PlaceHolderQueryControl -->
     <asp:PlaceHolder runat="server" id="PlaceHolderQueryControl"/>
    </td>
    <td width='15px'>&#160;</td>
   </tr>
   <tr>
    <td width='15px'>
     <img src="/_layouts/images/blank.gif" width='15' height='1' alt="" />
    </td>
    <td class="ms-descriptiontext">
     <!-- PlaceHolderHtmlMessage -->
     <asp:PlaceHolder runat="server" id="PlaceHolderHtmlMessage"/>
    </td>
    <td width='15px'>
     <img src="/_layouts/images/blank.gif" width='15' height='1' alt="" />
    </td>
   </tr>
   <tr>
    <td width='15px'>&#160;</td>
    <td class="ms-descriptiontext" style="color:red;">
     <!-- PlaceHolderError -->
     <asp:PlaceHolder runat="server" id="PlaceHolderError"/>
    </td>
    <td width='15px'>&#160;</td>
   </tr>
   <tr height='100%'>
    <td width='15px'>
     <img src="/_layouts/images/blank.gif" width='15' height='200' alt="" />
    </td>
    <td>
     <table cellspacing="0" cellpadding="0" width='100%' height='100%' 
              class="ms-pickerresultoutertable">
      <tr height="100%">
       ...
       <td id="ResultArea">
        <div id='resultcontent' class="ms-pickerresultdiv">
         <!-- PlaceHolderResultControl -->
         <asp:PlaceHolder runat="server" id="PlaceHolderResultControl"/>
        </div>
       </td>
      </tr>
     </table>
    </td>
    <td width='15px'>
     <img src="/_layouts/images/blank.gif" width='15' height='200' alt="" />
    </td>
   </tr>
   ...
   <tr id="EditorRow" runat="server">
    <td width='15px'>&#160;</td>
    <td width="100%">
     <table width="100%" cellspacing="0" cellpadding="0">
      <tr>
       <td>
        <input type='button' id='AddSel' runat='server' disabled
          class='ms-NarrowButtonHeightWidth' onclick='addSelected_Click();' 
          accesskey="<%$Resources:wss,picker_AddSelAccessKey%>" />
       </td>
       <td width="10px">
        <img src="/_layouts/images/blank.gif" width='4' height='1' alt="" />
       </td>
       <td width='100%'>
        <!-- PlaceHolderEditorControl -->
        <asp:PlaceHolder runat="server" id="PlaceHolderEditorControl"/>
       </td>
      </tr>
     </table>
    </td>
    <td width='15px'>&#160;</td>
   </tr>
   ...
  </table>
...
</asp:Content>

Clicking the Browse button of PeopleEditor requests the Picker.aspx with a number of query string parameters. Below is an example of such url after url decoding (indents and formatting are added for clarity):

http://servername/mysitecollection/_layouts/Picker.aspx?
      MultiSelect=True&
      CustomProperty=User,SecGroup,DL;;15;;;False&
      DialogTitle=Select People and Groups&
      DialogImage=/_layouts/images/ppeople.gif&
      PickerDialogType=Microsoft.SharePoint.WebControls.PeoplePickerDialog, 
                       Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, 
                       PublicKeyToken=71e9bce111e9429c&
      ForceClaims=False&
      DisableClaims=False&
      EnabledClaimProviders=&
      EntitySeparator=;;??????&
      DefaultSearch=

Among search and dialog options there is such parameter as PickerDialogType containing a fully qualified type name, in this case it’s PeoplePickerDialog. Having read this parameter the Picker.aspx creates an instance of PeoplePickerDialog through the Reflection. Below is the shortened code doing that, this code is taken from the OnLoad method of the Picker.aspx:

...
// get the type
Type type = Utility.GetTypeFromAssembly(Request["PickerDialogType"], true, false);
...        
// create instance through Reflection
this.DialogControl = (PickerDialog) Activator.CreateInstance(type);
	
...
// set some search options based on query string parameters
this.DialogControl.CustomProperty = Request["CustomProperty"];
...
string str3 = Request["MultiSelect"];
if (str3 != null)
  this.DialogControl.MultiSelect = bool.Parse(str3);

PeoplePickerDialog

PeoplePickerDialog, a control the main goal of which is to supply with other visual controls to be put into PlaceHolders of the Picker.aspx. So, the PeoplePickerDialog creates some controls and exposes references to them as properties, but doesn’t add them to its inner Controls-collection and, therefore, doesn’t render them on its own. Let’s see what controls are created by PeoplePickerDialog. Below are constructors of the PeoplePickerDialog and its base class, PickerDialog, respectively:

// the constructor of PeoplePickerDialog
public PeoplePickerDialog() : base(new PeopleQueryControl(), 
                                   new HierarchyResultControl(), 
                                   new PeopleEditor(), true)
{
    ...
}
	
// the main constructor of the base class, PickerDialog
public PickerDialog(PickerQueryControlBase query, PickerResultControlBase result, 
                    EntityEditorWithPicker editor)
{
   ...		
   this.ErrorMessageLabel = new Label();
   // the ErrorMessageLabel field is available through the ErrorLabel property
		
   this.m_HtmlMessageLabel = new Label();
   // the m_HtmlMessageLabel field is available through the HtmlMessageLabel property

   this.QueryControlValue = query;
   // the QueryControlValue field is available through the QueryControl property
		
   this.ResultControlValue = result;
   // the ResultControlValue field is available through the ResultControl property
		
   this.EditorControlValue = editor;
   // the EditorControlValue field is available through the EditorControl property
   ...
}

As we can see, when the Picker.aspx instantiates the object of PeoplePickerDialog through the Reflection a number of visual controls are created. How does the Picker.aspx use them? Below is the code taken again from the OnLoad method of the Picker.aspx:

...
// add the PeoplePickerDialog itself to the PlaceHolderDialogControl of the picker.aspx.   
this.PlaceHolderDialogControl.Controls.Add(this.DialogControl);
   
// add controls supplied by the PeoplePickerDialog to some PlaceHolders of the picker.aspx
this.PlaceHolderEditorControl.Controls.Add(this.DialogControl.EditorControl);
this.PlaceHolderResultControl.Controls.Add(this.DialogControl.ResultControl);
this.PlaceHolderQueryControl.Controls.Add(this.DialogControl.QueryControl);
...
// add controls supplied by the PeoplePickerDialog to some PlaceHolders of the picker.aspx   
this.PlaceHolderError.Controls.Add(this.DialogControl.ErrorLabel);
this.PlaceHolderHtmlMessage.Controls.Add(this.DialogControl.HtmlMessageLabel);

The picture below demonstrates where the basic controls are located within the dialog:

Basic Controls of the Search Dialog

Note the HtmlMessageLabel and ErrorLabel are not presented in the picture, but they reside in the appropriate PlaceHolders between PeopleQueryControl and HierarchyResultControl.

As we remember the PeopleEditor opens the search dialog. Interestingly that the dialog uses the same PeopleEditor control to hold selected users and groups. Note, however, the PeopleEditor within the dialog hides its buttons (Check Names and Browse) and doesn’t allow typing anything.

Besides creating and exposing the controls the PeoplePickerDialog also provides with some JavaScripts, exposes search parameters, defines some elements of the dialog’s appearance (title, for example), defines columns to be displayed in the table of search results and so on.

Related posts: