Friday, November 19, 2010

How to make an _layouts page (application page) in SharePoint anonymous?

Scenario:
How to make an _layouts page (application page) in SharePoint anonymous?

Explanation:
The application pages (custom pages that are created inside the layouts folder) in SharePoint  will ask for authentication even when a user tries to access them from a site that is configured for anonymous access (public facing site).

Resolution:

  • Make sure that the application page is inherited from "Microsoft.SharePoint.WebControls.UnsecuredLayoutsPageBase"
  • Override the AllowAnonymousAccess property and return true
If the application page does not have code-behind, the below snippet will mark the property "AllowAnonymousAccess" to true:

<%@ Page Language="C#" AutoEventWireup="true" Inherits="Microsoft.SharePoint.WebControls.LayoutsPageBase" DynamicMasterPageFile="~masterurl/default.master" %>
<script runat="server" type="text/C#">
protected override bool AllowAnonymousAccess
{  
    get
    {  
        return true;  
    }  
}
</script>

If the application page has code-behind, the below snippet will mark the property "AllowAnonymousAccess" to true:

public class ApplicationPage :  Microsoft.SharePoint.WebControls.UnsecuredLayoutsPageBase
{
  protected override bool AllowAnonymousAccess { get { return true; }
}

Tuesday, November 16, 2010

How to repair corrupt SPWebConfigModification Objects in SharePoint?

Scenario: How to repair corrupt SPWebConfigModification objects?

Explanation: Recently I worked on automating web.config deployments through PowerShell and during that process, I have ended up corrupting the SPWebConfigModification objects several times.

An indication of corrupted SPWebConfigModification objects is the generic exception "Object reference not set to instance of an object" when you try to make any changes to web.config using SPWebConfigModification class from code.

The below steps could be followed to fix the corrupted SPWebConfigModification objects:

Navigate to the table "Objects" in database "SharePoint_Config" and run the below script:

SELECT    Id, ClassId, ParentId, Name, Status,  Version, Properties
FROM        Objects
WHERE    (Name LIKE '%WebConfig%') 

In order to make any updates this table, we will need to disable the triggers on it.
By default,  the table "Objects" has the following triggers attached to it:
  • trigger_OnDeleteObjects
  • trigger_OnTouchObjects


DISABLE TRIGGER [dbo].[trigger_OnDeleteObjects] ON Objects
DISABLE TRIGGER [dbo].[trigger_OnTouchObjects] ON Objects

Perform an IISRESET.

Identify the corrupted row and delete it.

I have also tried to modified the corrupted item but it did not work
out. 

Enable triggers on the Objects table:

ENABLE TRIGGER [dbo].[trigger_OnDeleteObjects] ON Objects
ENABLE TRIGGER [dbo].[trigger_OnTouchObjects] ON Objects

Reference: thekid.me.uk

Friday, October 1, 2010

How to apply the SharePoint site's default master page on an application page (page in _layouts folder)

Scenario: How to apply the SharePoint site's default master page on an application page (page in _layouts folder)

Explanation: When we try to change the master page (~/_layouts/application.master) for an application page, SharePoint would not allow that change to happen.

Simple work around would be to change the master page in the "OnPreInit" event of the application page

A simple inline script would do the job:

<script runat="server">
protected override void OnPreInit(EventArgs e)
{
  base.OnPreInit(e);
  this.MasterPageFile = SPContext.Current.Web.MasterUrl;
}
</script> 

Complete code for the application page looks like the below:

<script runat="server">
protected override void OnPreInit(EventArgs e)
{
  base.OnPreInit(e);
  
  //This is where you set the master page to be used by this application page.
  //You can set the desired master page here. 
  //For this demo, I have set it to use the master page used by the SharePoint site this application page is called from
  this.MasterPageFile = SPContext.Current.Web.MasterUrl;
}
</script>

<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%> 
<%@ Page Language="C#" MasterPageFile="~/_layouts/application.master" Inherits="Microsoft.SharePoint.WebControls.LayoutsPageBase" %>
<%@ Import Namespace="Microsoft.SharePoint" %>

<asp:Content ID="Main" runat="server" contentplaceholderid="PlaceHolderMain">
    <!-- Whatever goes into the content area here -->
</asp:Content>

<asp:Content ID="PageTitle" runat="server" contentplaceholderid="PlaceHolderPageTitle" >
   Page title here
</asp:Content>

<asp:Content ID="PageTitleInTitleArea" runat="server" contentplaceholderid="PlaceHolderPageTitleInTitleArea" >
   Title in title area here
</asp:Content> 

Thursday, September 23, 2010

How to implement your own custom web part zone in SharePoint?

Scenario:How to implement your own custom web part zone in SharePoint?

Explanation:
Have you ever wondered whether it is possible to implement your own custom web part zone that gives you full ability to control how the web parts in a web part zone are added to a page?

By default, MOSS 2007 as well as SharePoint 2010 render the web parts in a table.
As most of us know, HTML tables are meant to be used for displaying tabulated data but not for controlling layout of a page. Tables were created to provide structure to data.

Even though SharePoint 2010 moved away from the table approach to the div approach, few areas remain untransitioned, the "Web Part Zone" being one of them.

Its often an painespecially for developers/designers to get the exact output to match that of the markup provided by a 3rd party company / designer (Yes it is quite often the case that SharePoint developers need to output HTML that need to match the markup provided by the client) and some times requires lot of effort to tweak their CSS/HTML Markup and also to ensure that the page out put is XHTML complaint especially in MOSS 2007.

Typical output from an OOB SharePoint Web Part Zone looks like the below:


As you can see in the above screenshot, SharePoint renders web parts in a table.

If you use the custom web part zone that I have also contributed in Codeplex (credits go to Tim Nugiel from MSNGN), you will be able to control the way the web parts added to a web part zone are added to a page.


As you can see in the above screen shot, the custom web part zone allows you to control the way the web parts are rendered on the page. The above screenshot demonstrates a web part zone with a Content Editor Web Part and a Page Viewer Web Part.

In this sample code, I have just added web parts one after the other.
You might want to add each web part in a new panel, apply css to them / seperate them with a <hr> / whatever you want to do

 If you want to be able to add user controls to the page, you will need a text field/column in the SharePoint Page that will be used to define the user controls to be added to this page.

If there are multiple user controls that are required to be added to the page, seperate them with a ";"
To use this web part zone in a page layout, register the control by adding te below line to the page layout
<%@ Register TagPrefix="Custom" TagName="WebPartZone" Src="~/_controltemplates/CustomWebPartZone/CustomWebPartZone.ascx" %>
Add the control to the page layout at the desired location
<div id="divCustomWPZone">
    <Custom:WebPartZone runat="server" id="zoneCustom" DisplayTitle="Custom Zone" LoadWebPartZone="true"/>
</div>
If you want be to able to load user controls as well, just use the FieldName="Name of the page field that contains the user control(s) to add to the page"

Download Source Code

Tuesday, September 21, 2010

Microsoft Security Advisory 2416728 (Vulnerability in ASP.NET) and SharePoint.

Microsoft recently released a Microsoft Security Advisory about a security vulnerability in ASP.NET.

This post explains the impact on SharePoint and documents a recommended workaround.
This vulnerability affects Microsoft SharePoint 2010 and Microsoft SharePoint Foundation 2010.  The vulnerability is in ASP.NET.

Microsoft recommends that all SharePoint 2010 customers apply the workaround as soon as possible.  This post will be updated with any new information.

The workaround for SharePoint 2010 is slightly different from the one documented in the advisory.
For SharePoint 2010, you should follow the instructions here on every web front-end in your SharePoint farm

Friday, September 17, 2010

Loading a SharePoint Web Part asynchronously

Scenario:
How to load a SharePoint Web Part asynchronously?

Explanation:
Web Parts that require long loading time might significantly slow down the overall page performance by increasing the page load time.

This is especially true with SharePoint search web parts, web parts that interact with third party web services, web parts that deal with large SharePoint lists, etc…

I have created "Asynchronous Web Part Framework" that enables web parts to load asynchronously.

To load pages asynchronously element by element facebook-style to speed up the first display of the page the first choice would probably not be UpdatePanel as it is meant for partial page updates as opposed to partial page loads. Web parts using Asynchronous Web Part Framework can in fact leverage the UpdatePanel for partial page updates as well.

Web Parts that utilize the Asynchronous Web Part Framework support all the features of SharePoint web parts such as editor web part, web part connections etc…

The Asynchronous Web Part Framework can be applied to existing web parts with very few code changes.
The same framework can also be used for developing web parts for use in Asp.Net applications. (With few file and path changes to replace the SharePoint layouts folder, etc...)

As soon as the page is loaded, the web part loads and displays a spinner icon.




Then the web part properties, web part connection information are sent to the web part web service asynchronously and the HTML mark is them loaded into the web part asynchronously.





What is required?

A web part that utilizes the Asynchronous Web Part framework should consist of the below components:
  •  A Web Part control class that serves as a wrapper for collecting properties, connections
  •  A Web Part UI control class that is responsible for rendering HTML for the web part

    • This control contains all the web part logic
  •  A Web Service that loads the web part UI control and renders its HTML back to the web part
  •  A JavaScript file that defines handlers for interaction between the web part and web service
How does it work?

  1. The web part control acts as a wrapper for collecting properties from either the web part properties pane or editor part and for connections with other web parts.
  2. When the web part loads first, the content container in the web part is loaded with a spinner and the JavaScript handler is registered with the page
  3. The JavaScript method then sends the web part properties and connection data to the web service
  4. The web service loads the web part UI control with the web part properties / connection details
  5. Web Part is then rendered as html
  6. If caching is required to be applied, the control HTML is stored in cache
  7. The result HTML is then loaded into the web part content container



This project contains working version of a basic web part that is loaded asynchronously.

This code also demonstrates how web part connections and editor part can be implemented with an asynchronous web part.

This code is created using Visual Studio 2010 and works with ASP.NET 3.0 or higher, MOSS 2007 and SharePoint 2010.

The AsyncWebPart.cs control which acts as the web part wrapper calls the JavaScript method "AsyncWebPartGetData" with all the web part parameters / connection data



The JavaScript method "AsyncWebPartGetData" calls the web service method "GetUIHtml" from web service "AsyncWebPartService" with all the parameters



The Web Service method "GetUIHtml" loads the "AsynWebPartUI" control which performs the actual core functionality of the web part.

This method renders the "AsyncWebPartUI" control as Html.



The AsyncWebPartUI control performs the actual core functionality of the web part.

Friday, September 10, 2010

SharePoint 2010 - Open PDF files in browser and set file association icon

Those who have experience with SharePoint 2010 may have noticed that pdf files in SharePoint 2010 do not open in browser by default. SharePoint 2010 throws a prompt to save the pdf file.

This is by behaviour in SharePoint 2010 and is very annoying especially when you have a requirement to show a pdf file in a Page Viewer Web Part, etc...

Luckily there is a workaround to fix this issue.

Open Central Admin > Application Management > Manage Web Applications.
Choose a web application and click "General Settings".


Scroll towards the bottom of the page until you find the section "Browser File Handling".
Change the option from "Strict" to "Permissive".


Go back to the document library and click on the pdf file. It should open in the browser. (Perform an IISRESET if required.)

You may also have noticed that the icons for pdf files do not show up by default in SharePoint 2010.

In order to fix it, we will need to set up document icon associations to inform SharePoint 2010 about the extension "pdf"

Navigate to 14 hive (“C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14”) and then to TEMPLATE\IMAGES. Upload the desired file icon (pdf in this case. You can use this icon for pdf)


Navigate to 14 hive, then to TEMPLATE\XML folder.
Locate the file "DOCICON.XML" and open it with a compatible editor.


Add the below line of text as required for the file extension (PDF mapping in this case):
<Mapping Key="pdf" Value="icpdf.png"/>


Perform an IISRESET and refresh the document library.
You should now see pdf icon next to the pdf documents.


 Hope that this post helps someone.

Tuesday, August 17, 2010

How to make SharePoint 2010 Content Query Web Part (CQWP) AJAX Enabled

Scenario:
How to make SharePoint 2010 Content Query Web Part (CQWP) AJAX Enabled

Explanation:
Those of you reading this post and have not yet explored the newly added AJAX options in the List View Web Parts in SharePoint 2010, they come with a category in the web part properties for "AJAX Options".


Enabling these options in the List View Web Part allows asynchronous load, update and refresh of the content being displayed. For ex: As highlighted in the image below, a "refresh" button is added if the option "Show Manual Refresh Button".


Unfortunately the categories that contain the AJAX options is not found in the Content Query Web Part popularly known as "CQWP" by sharepointers.



Fortunately as the CQWP inherits from the Data Form Web Part, the AJAX options are natively available in it. When you export a CQWP and edit it in a compatible application such as notepad, you will find the below properties:

<property name="InitialAsyncDataFetch" type="bool">False</property>
<property name="AsyncRefresh" type="bool">False</property>
<property name="ManualRefresh" type="bool">False</property>
<property name="AutoRefresh" type="bool">False</property>
<property name="AutoRefreshInterval" type="int">60</property>

The above properties map as below:

InitialAsyncDataFetch: Enable Asynchronous Load

AsyncRefresh: Enable Asynchronous Update
ManualRefresh: Show Manual Refresh Button
AutoRefresh: Enable Asynchronous Automatic Refresh
AutoRefreshInterveral: Automatic Refresh Interval (seconds)

After setting the property "InitialAsyncDataFetch" to true, the web part loaded asynchronously as below:



Hope that this helps somebody

Thursday, July 15, 2010

Error message when you try to start User Profile Synchronization in SharePoint 2010: An update conflict has occurred, and you must re-try this action. The object UserProfileApplication Name=User Profile Service Application was updated by [User]

Symptoms:

When you try to start User Profile Synchronization (Full/Incremental) or start the "User Profile Synchronization Service" found under "Services on server" in SharePoint 2010, you receive an error
"An update conflict has occurred, and you must re-try this action. The object UserProfileApplication Name=User Profile Service Application was updated by [User]"

Full details about this error as it is logged in various places has been detailed below:

ULS:

ConcurrencyException: Old Version : 241817 New Version : 241817 99a7a04b-0297-4ecc-9621-a38b05c8ccce Microsoft.SharePoint.Administration.SPUpdatedConcurrencyException: An update conflict has occurred, and you must re-try this action. The object UserProfileApplication Name=User Profile Service Application was updated by [User], in the OWSTIMER (3456) process, on machine [Server Name]

ULS:

SharePoint Foundation Runtime Unexpected Microsoft.SharePoint.Administration.SPUpdatedConcurrencyException: An update conflict has occurred, and you must re-try this action. The object UserProfileApplication Name=User Profile Service Application was updated by [User], in the OWSTIMER (3456) process, on machine [Server Name]

Event Viewer:

The Event 6482 is logged once every minute and has the following data:
An update conflict has occurred, and you must re-try this action. The object SearchDataAccessServiceInstance was updated by [User], in the OWSTIMER (1756) process, on machine [Server Name].

Troubleshooting:

Our initial thoughts went in the direction of Active Directory. We have started investigating whether any changes have been made to the AD that may be causing the issue. After confirming that no AD changes have been made, we tried to see why it is failing using the "Microsoft Forefront Identity Manager 2010" as it provides step by step detail about what happens. The "Microsoft Forefront Identity Manager 2010" failed to start because the "User Profile Synchronization Service" and the forefront windows services (Forefront Identity Manager Service and Forefront Identity Manager Synchronization Service) were in stopped state. When we tried to start the "User Profile Synchronization Service", we got the same error detailed above.
We took the whole farm backup and investigated further.

Cause:

This issue occurs if the contents of the file system cache on the front-end servers are newer than the contents of the configuration database. After you perform a system recovery, you may have to manually clear the file system cache on the local server.

Resolution:
To resolve this issue, clear the file system cache on all servers in the server farm on which the Windows SharePoint Services Timer service is running.

Microsoft has provided a step by step procedure on clearing file system cache from the SharePoint front-end servers in this kb article.


  1. Stop the Windows SharePoint Services Timer service (Found in Windows Services)
  2. Navigate to the cache folder
    In Windows Server 2008, the configuration cache is in the following location:
    Drive:\ProgramData\Microsoft\SharePoint\Config
    In Windows Server 2003, the configuration cache is in the following location:
    Drive:\Documents and Settings\All Users\Application Data\Microsoft\SharePoint\Config
    Locate the folder that has the file "Cache.ini"
    (Note: The Application Data folder may be hidden. To view the hidden folder, change the folder options as required)
  3.  Back up the Cache.ini file.
  4. Delete all the XML configuration files in the GUID folder. Do this so that you can verify that the GUID folder is replaced by new XML configuration files when the cache is rebuilt.
  5. Note When you empty the configuration cache in the GUID folder, make sure that you do not delete the GUID folder and the Cache.ini file that is located in the GUID folder.
  6. Double-click the Cache.ini file.
  7. On the Edit menu, click Select All. On the Edit menu, click Delete. Type 1, and then click Save on the File menu. On the File menu, click Exit.
  8. Start the Windows SharePoint Services Timer service
  9. Note The file system cache is re-created after you perform this procedure. Make sure that you perform this procedure on all servers in the server farm.
  10. Make sure that the Cache.ini file in the GUID folder now contains its previous value. For example, make sure that the value of the Cache.ini file is not 1.

Wednesday, July 7, 2010

How to create an External Content Type in SharePoint Designer 2010 using Business Connectivity Services(BCS) and fix issues that arise on the way

Scenario:
How to create an External Content Type in SharePoint Designer 2010 using Business Connectivity Services(BCS) and fix issues that arise on the way

Explanation:
In this walkthrough I will explain how to use Sharepoint Server 2010 Business Connectivity Services(BCS) feature to access external business data (SQL Server 2008 in this example). This simple step-by-step will also help you fix the issues that you might encounter on the way

Create Model using SharePoint Designer

SPD includes functionality to design the application definition model visually. Based on the options selected on UI, it generates the xml metadata in the background. Using ECT Designer in SPD you can discover database, point to the table, view, or stored procedure that will perform the operations, and then return the required data and use it to create external content type without writing any code or XML. Follow the steps below to create the ECT:

Open up SharePoint Designer 2010 and click on "External Content Types"

External Content Types

To create a new external content type, click on "New External Content Type" in the ribbon



Click on the link "Click here to discover external data sources and define operations". This will open up the windows to define the connection to AW database and operations for the ECT.

Click "Add Connection" under External Data Source section and choose Data Source Type as SQL Server. This brings up the SQL connection properties dialog. In this we are connecting using SQL Server provider to get data.

Define Operations on External System

SPD provides option to create the view for all common operations available in BCS or it can create operations for specific operation.

Following two minimum operations are required to fetch data from backend using BCS:

  • Query Item List method which gets the list of records and work as finder method
  • Read Item method which gets data for specific record and work as SpecificFinder method

Choose the appropriate external data connection and then the database table. Right click on the selected table and create operations as required. In this example, I have created all the operations that are possible through SPD 2010.

External Data Connection

After adding all the operations, we should be able to see something like in the image below:



Create External List based on External Content Type

You can create an external content type by using Microsoft SharePoint Designer 2010 or the browser. Follow the steps given below to create list using browser.

  1. Open the SharePoint site in which you would like to create the external list in browser.
  2. Go to Site Actions, View All Site Content.
  3. Click the Create button. In the Custom Lists section, click External List.
  4. On the New page, type the list name and description for the new external list.
  5. The Data source configuration section displays a text box and an external content type picker. Use the picker to choose the external content type. Select the newly created external content type and then click OK.
  6. Click Create.
This creates the external list. You can now navigate to the new list in the SharePoint site and view/edit items.





All good so far. But you can expect to see the below error when we try to access the external list that has been just created.

Access Denied Error

This is because the BDC service that we just created has not been given permissions yet.

Open Central Admin > Application Management > Manage Service Applications > Business Data Connectivity Service and select the check box next to the service that we just created and then click "Set Object Permissions". Add the user(s) that need to be given access as in the image below:



Go back to the external list and refresh the page if required.
Now we see a new error "Login failed for user 'NT Authority\ANONYMOUS LOGON" as in the image below:

Login failed error

The above error occured because by default, when we create the BDC definition in SPD 2010, the authentication mode is set to "User's Identity".

The "Connect with User’s Identity" is the "PassThrough" authentication mode we had in MOSS 2007 BDC. The other 2 relates to SSO. Now that we have Secure Store Service Application, we can use "Connect with Impersonated Windows Identity" OR if we are using claims token we can use "Connect with Impersonated Custom Identity"

Inorder to access the data from the external data connection, one way of fixing the above issue is to change the Authentication Mode from "User's Identity" to "BDC Identity".

So open up the external content type in SPD 2010 and change the authentication mode.



Now we end up with a new error:

Change Authentication Mode Error

Below are steps we need to follow to get this corrected!

We have to first enable BCS model to accept "RevertToSelf" as one of the authentication modes. Yes, it’s disabled by default. We can do this using SharePoint 2010 Management Console.

The "ReverToSelfAllowed" property is set to false by default. We can now change it to true using the below script:

$bdc = Get-SPServiceApplication | where {$_ -match "Business Data Connectivity Service"};
$bdc.RevertToSelfAllowed = $true;
$bdc.Update();

So finally when we hit the list again, we should be able to see the rows from the SQL Server table as items in the external list that we have created. Also notice the highlighted top left corner in the image below.
We are able to see the options "New Item", "View Item", "Edit Item" and "Delete Item" because I have created all the operations from SPD 2010 when I created the BDC definition above. If you skip any of the operations for example "Delete Operation", the "Delete Item" option will be diabled in the ribbon.


Friday, July 2, 2010

Effect of moving PerformancePoint Service from one server in a SharePoint 2010 farm to another..

Scenario:
We have 2 WFE servers in a SharePoint 2010 farm.
We had PerformancePoint Service residing on WFE1 and for enhancing performance, we needed to move it to WFE2.
Before moving the service we had one dashboard page that was using PerformancePoint Service.



After movied the services from WFE1 to WFE2, all the web parts on the page disappeared and we ended up with the below:



Explanation:
Plan your farm well in advance and be cautious and take the necessary backups before attempting to move the services from one server to the other in a farm.

Saturday, June 26, 2010

SharePoint 2010 - Application Page runtime error "Server Error in "/" Application"

Scenario:

I have developed an application page in SharePoint 2010. After deploying it as a solution, I got the below error:

Server Error in '/' Application. 
--------------------------------

Runtime Error

<!-- Web.Config Configuration File -->

<configuration>
  <system.web>
    <customErrors mode="Off"/>
  </system.web>
</configuration> 

Notes: The current error page you are seeing can be replaced by a custom error page by modifying the "defaultRedirect" attribute of the application's <customErrors> configuration tag to point to a custom error page URL.

<!-- Web.Config Configuration File -->

<configuration>
  <system.web>
    <customErrors mode="RemoteOnly" defaultRedirect="mycustompage.htm"/>
  </system.web>
</configuration> 

So, I went in and turn customError and StackTrace option, in web.config on; and then, I got a more detailed error :). Runtime Error with the description telling me that I need to set customError and StackTrace on to see the message. This sound very recursive and very strange. Since, I could not see this error that is why I try to turn on by changing customError and StackTrace on and now they are both on, I am suddenly getting the message telling me to do so in place of the SharePoint custom error.

Explanation:

In SharePoint 2010, depending on what we do, we may need to also edit the web.config in the layouts folder of the SharePoint root:
C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\web.config

Tuesday, June 22, 2010

"Internet Explorer cannot display the webpage" error in SharePoint 2010 web application

While working on a SharePoint 2010 site, I suddenly encountered the infamous error "Internet Explorer cannot display the web page".

It was just me who was working on that farm at that time.

When I attempted to access the same site collection from within the server, I have been thrown the login prompt repeatedly despite providing the right credentials. All the other site collections in other web applications were behaving normally.

So at this point, it looks like it has to do some thing with that particular web application. I started to check all the web application properties, performed IISRESET but nothing worked. So I rebooted all the servers in the farm starting with the WFEs, then the SQL Server and the AD server.

Finally after handling the Loopback check issue which is covered in this KB article: http://support.microsoft.com/kb/896861, the authentication issues have been resolved.

Friday, June 4, 2010

Displaying real time SharePoint search results alphabetically using search.asmx and jQuery/JS

Scenario:
Ho to display real time SharePoint people search results alphabetically using search.asmx and js

Explanation:
If there is a requirement to display all AD users in people search in SharePoint, the list can grow long and hence it is a good idea to categorize users alphabetically.

People Search Results

Solution:
The below combination of Javascript and XSL can be used to accomplish the above. You can either use your own custom search web part or the OOB search results web part to get the results xml and apply the below XSL to the xml result

The below XSL builds a list of alphabets as links. Upon clicking each link a javascript function is called and names of people that start with the corresponding alphabet are displayed.

<xsl:attribute name="onclick">
GetContacts('<xsl:value-of select="child::node()[position()]"/>');
</xsl:attribute>

Lets get started..

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
  <xsl:output method="html" encoding="UTF-8"/>
  <xsl:template name="Alphabets">
    <Alpha>A</Alpha>
    <Alpha>B</Alpha>
    <Alpha>C</Alpha>
    <Alpha>D</Alpha>
    <Alpha>E</Alpha>
    <Alpha>F</Alpha>
    <Alpha>G</Alpha>
    <Alpha>H</Alpha>
    <Alpha>I</Alpha>
    <Alpha>J</Alpha>
    <Alpha>K</Alpha>
    <Alpha>L</Alpha>
    <Alpha>M</Alpha>
    <Alpha>N</Alpha>
    <Alpha>O</Alpha>
    <Alpha>P</Alpha>
    <Alpha>Q</Alpha>
    <Alpha>R</Alpha>
    <Alpha>S</Alpha>
    <Alpha>T</Alpha>
    <Alpha>U</Alpha>
    <Alpha>V</Alpha>
    <Alpha>W</Alpha>
    <Alpha>X</Alpha>
    <Alpha>Y</Alpha>
    <Alpha>Z</Alpha>
  </xsl:template>
  <xsl:variable name="smallcase" select="'abcdefghijklmnopqrstuvwxyz'"/>
  <xsl:variable name="uppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
  <xsl:key name="keyTitle" match="Result" use="translate(substring(lastname,1,1),'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ')"/>
  <xsl:template match="/">
    <xsl:variable name="Rows" select="/Search_Results/All_Results/Result"/>
    <div id="assocDirResultBox" sizset="115" sizcache="16">
      <xsl:variable name="AlphabetNodes">
        <xsl:call-template name="Alphabets"/>
      </xsl:variable>
      <xsl:variable name="AlphabetName">
        <xsl:apply-templates select="msxsl:node-set($AlphabetNodes)/Alpha">
          <xsl:sort select="." order="ascending"/>
        </xsl:apply-templates>
      </xsl:variable>
      <xsl:variable name="TitleNames">
        <xsl:for-each select="$Rows[generate-id(.) = generate-id(key('keyTitle', translate(substring(lastname,1,1),$smallcase,$uppercase))[1])]">
          <xsl:value-of select="translate(substring(lastname,1,1),$smallcase,$uppercase)"/>
        </xsl:for-each>
      </xsl:variable>
      <a name="top_AssosiatesAToZ"></a>
      <div class="alphaBrowseNavBox" sizcache="16" sizset="111">
        <div class="alphaBrowseNav" sizcache="16" sizset="111">
          <xsl:for-each select="msxsl:node-set($AlphabetNodes)/Alpha">
            <xsl:if test="position() &lt;= count(msxsl:node-set($AlphabetNodes)/Alpha)">
              <xsl:variable name="char" select="child::node()[position()]"/>
              <xsl:choose>
                <xsl:when test="contains($TitleNames,$char)">
                  <a>
                    <xsl:attribute name="href">
                      #<xsl:value-of select="child::node()[position()]"/>
                    </xsl:attribute>
                    <xsl:attribute name="onclick">
                      GetContacts('<xsl:value-of select="child::node()[position()]"/>');
                    </xsl:attribute>
                    <xsl:value-of select="child::node()[position()]"/>
                  </a>
                </xsl:when>
                <xsl:otherwise>
                  <span>
                    <xsl:value-of select="child::node()[position()]"/>
                  </span>
                </xsl:otherwise>
              </xsl:choose>
            </xsl:if>
          </xsl:for-each>
        </div>
      </div>
      <div id="searchResults">
      </div>
    </div>
  </xsl:template>
  <!-- A generic function that can be used to replace strings in a given text. Similar to String.Replace of C# -->
  <xsl:template name="ReplaceAllCharsInString">
    <xsl:param name="text"/>
    <xsl:param name="replace"/>
    <xsl:param name="by"/>
    <xsl:choose>
      <xsl:when test="contains($text, $replace)">
        <xsl:value-of select="substring-before($text,$replace)"/>
        <xsl:value-of select="$by"/>
        <xsl:call-template name="ReplaceAllCharsInString">
          <xsl:with-param name="text" select="substring-after($text,$replace)"/>
          <xsl:with-param name="replace" select="$replace"/>
          <xsl:with-param name="by" select="$by"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$text"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

The javascript/jQuery functions:
function GetContacts(alpha) 
  {
   var qry = "SELECT Department,WorkPhone,OfficeNumber,AboutMe,PictureURL,WorkEmail,WebSite,BaseOfficeLocation,LastName,FirstName,Company,MobilePhone,PreferredName,AccountName,UserProfile_GUID, JobTitle,Size, Rank, Path, Title, Description, Write  FROM SCOPE() WHERE (\"scope\"='Associates') AND (\"DAV:contentclass\"='urn:content-class:SPSPeople') AND (\"LastName\" LIKE '" + alpha + "%') ORDER BY LastName";

   var queryXML = 
    <![CDATA["<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'> \
    <Query domain='QDomain'> \
     <SupportedFormats><Format>urn:Microsoft.Search.Response.Document.Document</Format></SupportedFormats> \
     <Context> \
      <QueryText type='MSSQLFT' language='en-us'>#qry#</QueryText> \
     </Context> \
    <SortByProperties><SortByProperty name='Rank' direction='Descending' order='1'/></SortByProperties> \
     <Range><StartAt>1</StartAt><Count>2000</Count></Range> \
     <EnableStemming>false</EnableStemming> \
     <TrimDuplicates>true</TrimDuplicates> \
     <IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery> \
     <ImplicitAndBehavior>true</ImplicitAndBehavior> \
     <IncludeRelevanceResults>true</IncludeRelevanceResults> \
     <IncludeSpecialTermResults>true</IncludeSpecialTermResults> \
     <IncludeHighConfidenceResults>true</IncludeHighConfidenceResults> \
    </Query></QueryPacket>"]]>;
   
   queryXML = queryXML.replace('#qry#', qry);
   
   var soapEnv =
    <![CDATA["<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'> \
      <soap:Body> \
     <Query xmlns='urn:Microsoft.Search'> \
       <queryXml>" + escapeHTML(queryXML) + "</queryXml> \
     </Query> \
      </soap:Body> \
    </soap:Envelope>"]]>;   

   $.blockUI({ message: '<p>Please wait while the contacts are being loaded...</p>' });
    
   $.ajax({
    url: window.location.protocol + "//" + window.location.host + "/_vti_bin/search.asmx",
    type: "POST",
    dataType: "xml",
    data: soapEnv,
    complete: processResult,
    contentType: "text/xml; charset=\"utf-8\""
   });
   
   function processResult(xData, status) {
    var html = "";
    html += "&lt;div class='alphaBrowseNavHeader'&gt;" + alpha + "&lt;/div&gt;";
    html += "&lt;div class='rightActionsWrapper'&gt;";
    $(xData.responseXML).find("QueryResult").each(function() {
     var x = $("<xml>" + $(this).text() + "</xml>");

     /* Find value of each property */ 
     x.find("Document").each(function() {
      var department = '', workPhone = '', pictureUrl = '', workEmail = '', location = '', lastName = '', firstName = '', company = '', mobilePhone = '', preferredname = '', accountName = '', accountNameEncoded = '', jobTitle = '', responsibility = '';
      $(this).find("Property").each(function() {
       switch($(this).find("Name").text())
       {
        case 'DEPARTMENT':
         department = $(this).find("Value").text();
         break;
        case 'WORKPHONE':
         workPhone = $(this).find("Value").text();
         break;          
        case 'PICTUREURL':
         pictureUrl = $(this).find("Value").text();
         break;          
        case 'WORKEMAIL':
         workEmail = $(this).find("Value").text();
         break;          
        case 'BASEOFFICELOCATION':
         location = $(this).find("Value").text();
         break;          
        case 'LASTNAME':
         lastName = $(this).find("Value").text();
         break;          
        case 'FIRSTNAME':
         firstName = $(this).find("Value").text();
         break;          
        case 'COMPANY':
         company = $(this).find("Value").text();
         break;          
        case 'MOBILEPHONE':
         mobilePhone = $(this).find("Value").text();
         break;          
        case 'PREFERREDNAME':
         preferredname = $(this).find("Value").text();
         break;          
        case 'ACCOUNTNAME':
         accountName = $(this).find("Value").text();
         accountNameEncoded = accountName.replace('\\','%5C');
         break;          
        case 'JOBTITLE':
         jobTitle = $(this).find("Value").text();
         break;  
        case 'RESPONSIBILITY':
         responsibility = $(this).find("Value").text();
         break;  
        default:
         break;
       }
      })
      
      /* If no picture is available, display the temp place holder image */
      if(pictureUrl == '')
      {
       pictureUrl = '/Style%20Library/Images/temp_placeholder.gif';
      }          
      
      var title = $("Title", $(this)).text();
      var url = $("Action>LinkUrl", $(this)).text();
      var description = $("Description", $(this)).text()
       
      html += 
      <![CDATA["<div class='rightActionsBox'> \
       <a href='"+ url +"'><img alt='Photo of " + preferredname + "' border='0' width='158' class='rightActionsBoxImage' src='"+pictureUrl+"' /></a> \
       <div class='rightActionsCopy' sizset='122' sizcache='16'> \
        <a href='"+ url +"'><span class='name'>" + preferredname + "</span></a> \
        <table class='assocDirResultTable'> \
         <tbody> \
          <tr> \
           <td> \
            <span class='property title'>&#160;#jobTitle#</span> \
            <span class='property'><label>Work:</label>&#160;#workPhone#</span> \
            <span class='property'><label>Mobile:</label>&#160;#mobilePhone#</span> \
            <span class='property' sizset='122' sizcache='16'>#accountLink#</span> \
           </td> \
           <td> \
            <span class='property'><label>Business:</label>&#160;#company#</span> \
            <span class='property'><label>Department:</label>&#160;#department#</span> \
            <span class='property'><label>Location:</label>&#160;#location#</span> \
           </td> \
           <td class='last'><label>Ask me about</label> \
            <span class='property'>#responsibility#</span> \
           </td> \
          </tr> \
         </tbody> \
        </table> \
       </div> \
       <ul class='rightActions' sizset='123' sizcache='16'> \
        <li class='iconCal' sizset='123' sizcache='16'> \
         <a href='#' onclick='ShowDialog(#AddToColleageCall#, null, AddToColleaguesDialogCallback);return false;'>Add As Colleague</a> \
        </li> \
        <li class='iconPeople' sizset='124' sizcache='16'> \
         <a href='#' onclick='ShowDialog(#ShowOrganizationHierarchyCall#, null, ShowOrganizationHierarchyDialogCallback);return false;'>Browse In Organization Chart</a> \
        </li> \
        <li class='iconFlagGreen' sizset='125' sizcache='16'> \
         <a href='#' onclick='ShowDialog(#ViewRecentContentCall#, null, ViewRecentContentDialogCallback);return false;'>View Recent Content</a> \
        </li> \
       </ul> \
      </div>"]]>
      html = html.replace('#jobTitle#', jobTitle).replace('#workPhone#', workPhone).replace('#mobilePhone#', mobilePhone).replace('#company#', company).replace('#department#', department).replace('#location#', location).replace('#responsibility#', responsibility).replace('#AddToColleageCall#','"' + 'http://[Site URL]/my/_layouts/QuickLinksDialogForm.aspx?Mode=Person&amp;NTName=' + accountNameEncoded + '&amp;IsDlg=1' + '"').replace('#ShowOrganizationHierarchyCall#','"' + 'http://[site url]/my/OrganizationView.aspx?ProfileType=User&amp;accountname=' + accountNameEncoded + '"').replace('#ViewRecentContentCall#','"' + 'http://[site URL]/my/personcontent.aspx?accountname=' + accountNameEncoded + '"').replace('#workEmail#',workEmail);
      if(workEmail != '')
      {
       html = html.replace('#accountLink#','&lt;a href=\'mailto:#workEmail#\'&gt;#accountName#&lt;/a&gt;');
       html = html.replace('#workEmail#', workEmail);
       html = html.replace('#accountName#', accountName);
      }
      else 
       html = html.replace('#accountLink#',accountName);
     });
    });
    html += "&lt;/div&gt;"; 
    $("#searchResults").empty().append(html);
   }
  } 

Complete Code:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
 <xsl:output method="html" encoding="UTF-8"/>
 <xsl:template name="Alphabets">
  <Alpha>A</Alpha>
  <Alpha>B</Alpha>
  <Alpha>C</Alpha>
  <Alpha>D</Alpha>
  <Alpha>E</Alpha>
  <Alpha>F</Alpha>
  <Alpha>G</Alpha>
  <Alpha>H</Alpha>
  <Alpha>I</Alpha>
  <Alpha>J</Alpha>
  <Alpha>K</Alpha>
  <Alpha>L</Alpha>
  <Alpha>M</Alpha>
  <Alpha>N</Alpha>
  <Alpha>O</Alpha>
  <Alpha>P</Alpha>
  <Alpha>Q</Alpha>
  <Alpha>R</Alpha>
  <Alpha>S</Alpha>
  <Alpha>T</Alpha>
  <Alpha>U</Alpha>
  <Alpha>V</Alpha>
  <Alpha>W</Alpha>
  <Alpha>X</Alpha>
  <Alpha>Y</Alpha>
  <Alpha>Z</Alpha>
 </xsl:template>
 <xsl:variable name="smallcase" select="'abcdefghijklmnopqrstuvwxyz'"/>
 <xsl:variable name="uppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
 <xsl:key name="keyTitle" match="Result" use="translate(substring(lastname,1,1),'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ')"/>
 <xsl:template match="/">
  <script type="text/javascript">
  function AddToColleaguesDialogCallback(dialogResult, returnValue)
  {
   if (dialogResult == 1)
   {
    alert('Contact has been added as your colleague.');
   }
  }

  function ShowOrganizationHierarchyDialogCallback(dialogResult, returnValue)
  {
  }
  
  function ViewRecentContentDialogCallback(dialogResult, returnValue)
  {
  }
  
  function ULSX84(){var o=new Object;o.ULSTeamName="SharePoint Portal Server";o.ULSFileName="portal.js";return o;}
  
  function ShowDialog(e,b,c)
  {
   ULSX84:;
   var a=[];
   if(b!=null)
    a[0]=b;
   else 
    a=null;
   var d={width:750,height:500};
   SP.UI.ModalDialog.commonModalDialogOpen(e,d,c,a)
  }
 
  $(document).ajaxStop($.unblockUI); 
 
  $(document).ready(function(){
   GetContacts('A');
  });
 
  function GetContacts(alpha) 
  {
   var qry = "SELECT Department,WorkPhone,OfficeNumber,AboutMe,PictureURL,WorkEmail,WebSite,BaseOfficeLocation,LastName,FirstName,Company,MobilePhone,PreferredName,AccountName,UserProfile_GUID, JobTitle,Size, Rank, Path, Title, Description, Write  FROM SCOPE() WHERE (\"scope\"='Associates') AND (\"DAV:contentclass\"='urn:content-class:SPSPeople') AND (\"LastName\" LIKE '" + alpha + "%') ORDER BY LastName";

   var queryXML = 
    <![CDATA["<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'> \
    <Query domain='QDomain'> \
     <SupportedFormats><Format>urn:Microsoft.Search.Response.Document.Document</Format></SupportedFormats> \
     <Context> \
      <QueryText type='MSSQLFT' language='en-us'>#qry#</QueryText> \
     </Context> \
    <SortByProperties><SortByProperty name='Rank' direction='Descending' order='1'/></SortByProperties> \
     <Range><StartAt>1</StartAt><Count>2000</Count></Range> \
     <EnableStemming>false</EnableStemming> \
     <TrimDuplicates>true</TrimDuplicates> \
     <IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery> \
     <ImplicitAndBehavior>true</ImplicitAndBehavior> \
     <IncludeRelevanceResults>true</IncludeRelevanceResults> \
     <IncludeSpecialTermResults>true</IncludeSpecialTermResults> \
     <IncludeHighConfidenceResults>true</IncludeHighConfidenceResults> \
    </Query></QueryPacket>"]]>;
   
   queryXML = queryXML.replace('#qry#', qry);
   
   var soapEnv =
    <![CDATA["<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'> \
      <soap:Body> \
     <Query xmlns='urn:Microsoft.Search'> \
       <queryXml>" + escapeHTML(queryXML) + "</queryXml> \
     </Query> \
      </soap:Body> \
    </soap:Envelope>"]]>;   

   $.blockUI({ message: '<p>Please wait while the contacts are being loaded...</p>' });
    
   $.ajax({
    url: window.location.protocol + "//" + window.location.host + "/_vti_bin/search.asmx",
    type: "POST",
    dataType: "xml",
    data: soapEnv,
    complete: processResult,
    contentType: "text/xml; charset=\"utf-8\""
   });
   
   function processResult(xData, status) {
    var html = "";
    html += "&lt;div class='alphaBrowseNavHeader'&gt;" + alpha + "&lt;/div&gt;";
    html += "&lt;div class='rightActionsWrapper'&gt;";
    $(xData.responseXML).find("QueryResult").each(function() {
     var x = $("<xml>" + $(this).text() + "</xml>");

     /* Find value of each property */ 
     x.find("Document").each(function() {
      var department = '', workPhone = '', pictureUrl = '', workEmail = '', location = '', lastName = '', firstName = '', company = '', mobilePhone = '', preferredname = '', accountName = '', accountNameEncoded = '', jobTitle = '', responsibility = '';
      $(this).find("Property").each(function() {
       switch($(this).find("Name").text())
       {
        case 'DEPARTMENT':
         department = $(this).find("Value").text();
         break;
        case 'WORKPHONE':
         workPhone = $(this).find("Value").text();
         break;          
        case 'PICTUREURL':
         pictureUrl = $(this).find("Value").text();
         break;          
        case 'WORKEMAIL':
         workEmail = $(this).find("Value").text();
         break;          
        case 'BASEOFFICELOCATION':
         location = $(this).find("Value").text();
         break;          
        case 'LASTNAME':
         lastName = $(this).find("Value").text();
         break;          
        case 'FIRSTNAME':
         firstName = $(this).find("Value").text();
         break;          
        case 'COMPANY':
         company = $(this).find("Value").text();
         break;          
        case 'MOBILEPHONE':
         mobilePhone = $(this).find("Value").text();
         break;          
        case 'PREFERREDNAME':
         preferredname = $(this).find("Value").text();
         break;          
        case 'ACCOUNTNAME':
         accountName = $(this).find("Value").text();
         accountNameEncoded = accountName.replace('\\','%5C');
         break;          
        case 'JOBTITLE':
         jobTitle = $(this).find("Value").text();
         break;  
        case 'RESPONSIBILITY':
         responsibility = $(this).find("Value").text();
         break;  
        default:
         break;
       }
      })
      
      /* If no picture is available, display the temp place holder image */
      if(pictureUrl == '')
      {
       pictureUrl = '/Style%20Library/Images/temp_placeholder.gif';
      }          
      
      var title = $("Title", $(this)).text();
      var url = $("Action>LinkUrl", $(this)).text();
      var description = $("Description", $(this)).text()
       
      html += 
      <![CDATA["<div class='rightActionsBox'> \
       <a href='"+ url +"'><img alt='Photo of " + preferredname + "' border='0' width='158' class='rightActionsBoxImage' src='"+pictureUrl+"' /></a> \
       <div class='rightActionsCopy' sizset='122' sizcache='16'> \
        <a href='"+ url +"'><span class='name'>" + preferredname + "</span></a> \
        <table class='assocDirResultTable'> \
         <tbody> \
          <tr> \
           <td> \
            <span class='property title'>&#160;#jobTitle#</span> \
            <span class='property'><label>Work:</label>&#160;#workPhone#</span> \
            <span class='property'><label>Mobile:</label>&#160;#mobilePhone#</span> \
            <span class='property' sizset='122' sizcache='16'>#accountLink#</span> \
           </td> \
           <td> \
            <span class='property'><label>Business:</label>&#160;#company#</span> \
            <span class='property'><label>Department:</label>&#160;#department#</span> \
            <span class='property'><label>Location:</label>&#160;#location#</span> \
           </td> \
           <td class='last'><label>Ask me about</label> \
            <span class='property'>#responsibility#</span> \
           </td> \
          </tr> \
         </tbody> \
        </table> \
       </div> \
       <ul class='rightActions' sizset='123' sizcache='16'> \
        <li class='iconCal' sizset='123' sizcache='16'> \
         <a href='#' onclick='ShowDialog(#AddToColleageCall#, null, AddToColleaguesDialogCallback);return false;'>Add As Colleague</a> \
        </li> \
        <li class='iconPeople' sizset='124' sizcache='16'> \
         <a href='#' onclick='ShowDialog(#ShowOrganizationHierarchyCall#, null, ShowOrganizationHierarchyDialogCallback);return false;'>Browse In Organization Chart</a> \
        </li> \
        <li class='iconFlagGreen' sizset='125' sizcache='16'> \
         <a href='#' onclick='ShowDialog(#ViewRecentContentCall#, null, ViewRecentContentDialogCallback);return false;'>View Recent Content</a> \
        </li> \
       </ul> \
      </div>"]]>
      html = html.replace('#jobTitle#', jobTitle).replace('#workPhone#', workPhone).replace('#mobilePhone#', mobilePhone).replace('#company#', company).replace('#department#', department).replace('#location#', location).replace('#responsibility#', responsibility).replace('#AddToColleageCall#','"' + 'http://[Site URL]/my/_layouts/QuickLinksDialogForm.aspx?Mode=Person&amp;NTName=' + accountNameEncoded + '&amp;IsDlg=1' + '"').replace('#ShowOrganizationHierarchyCall#','"' + 'http://[site url]/my/OrganizationView.aspx?ProfileType=User&amp;accountname=' + accountNameEncoded + '"').replace('#ViewRecentContentCall#','"' + 'http://[site URL]/my/personcontent.aspx?accountname=' + accountNameEncoded + '"').replace('#workEmail#',workEmail);
      if(workEmail != '')
      {
       html = html.replace('#accountLink#','&lt;a href=\'mailto:#workEmail#\'&gt;#accountName#&lt;/a&gt;');
       html = html.replace('#workEmail#', workEmail);
       html = html.replace('#accountName#', accountName);
      }
      else 
       html = html.replace('#accountLink#',accountName);
     });
    });
    html += "&lt;/div&gt;"; 
    $("#searchResults").empty().append(html);
   }
  } 
  
  <![CDATA[
  function escapeHTML (str) {
   return str.replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }
  ]]>
  </script>
  <xsl:variable name="Rows" select="/Search_Results/All_Results/Result"/>
  <div id="assocDirResultBox" sizset="115" sizcache="16">
   <xsl:variable name="AlphabetNodes">
    <xsl:call-template name="Alphabets"/>
   </xsl:variable>
   <xsl:variable name="AlphabetName">
    <xsl:apply-templates select="msxsl:node-set($AlphabetNodes)/Alpha">
     <xsl:sort select="." order="ascending"/>
    </xsl:apply-templates>
   </xsl:variable>
   <xsl:variable name="TitleNames">
    <xsl:for-each select="$Rows[generate-id(.) = generate-id(key('keyTitle', translate(substring(lastname,1,1),$smallcase,$uppercase))[1])]">
     <xsl:value-of select="translate(substring(lastname,1,1),$smallcase,$uppercase)"/>
    </xsl:for-each>
   </xsl:variable>
   <a name="top_AssosiatesAToZ"></a>
   <div class="alphaBrowseNavBox" sizcache="16" sizset="111">
    <div class="alphaBrowseNav" sizcache="16" sizset="111">
     <xsl:for-each select="msxsl:node-set($AlphabetNodes)/Alpha">
      <xsl:if test="position() &lt;= count(msxsl:node-set($AlphabetNodes)/Alpha)">
       <xsl:variable name="char" select="child::node()[position()]"/>
       <xsl:choose>
        <xsl:when test="contains($TitleNames,$char)">
         <a>
          <xsl:attribute name="href">
           #<xsl:value-of select="child::node()[position()]"/>
          </xsl:attribute>
          <xsl:attribute name="onclick">
           GetContacts('<xsl:value-of select="child::node()[position()]"/>');
          </xsl:attribute>          
          <xsl:value-of select="child::node()[position()]"/>
         </a>
        </xsl:when>
        <xsl:otherwise>
         <span>
          <xsl:value-of select="child::node()[position()]"/>
         </span>
        </xsl:otherwise>
       </xsl:choose>
      </xsl:if>
     </xsl:for-each>
    </div>
   </div>
   <div id="searchResults">
   </div>
  </div>
 </xsl:template>
 <!-- A generic function that can be used to replace strings in a given text. Similar to String.Replace of C# -->
 <xsl:template name="ReplaceAllCharsInString">
  <xsl:param name="text"/>
  <xsl:param name="replace"/>
  <xsl:param name="by"/>
  <xsl:choose>
   <xsl:when test="contains($text, $replace)">
    <xsl:value-of select="substring-before($text,$replace)"/>
    <xsl:value-of select="$by"/>
    <xsl:call-template name="ReplaceAllCharsInString">
     <xsl:with-param name="text" select="substring-after($text,$replace)"/>
     <xsl:with-param name="replace" select="$replace"/>
     <xsl:with-param name="by" select="$by"/>
    </xsl:call-template>
   </xsl:when>
   <xsl:otherwise>
    <xsl:value-of select="$text"/>
   </xsl:otherwise>
  </xsl:choose>
 </xsl:template>
</xsl:stylesheet>

Lessons learned from first SharePoint 2010 implementation

Just wanted to list down the lessons learnt from our first SharePoint 2010 implementation. While some of them are quite simple ones, some were disasterous.

  1. Attempted to save a blog site as a template. SharePoint has succesfully created a sandbox wsp solution in the solutions gallery of the site collection. But it could not be activated. ULS Logs told that there were some errors in the xml. So we cleaned up the xml and rebuild the wsp (Visual Studio 2010 has a option to open wsp files and reverse engineer them). Uploaded the solution into solutions gallery. Activated the solution. At first it looked as if every thing went smooth. All fine. But later we have noticed that it corrupted the whole site collection taxanomy. We have noticed that the Site Columns and Content Types galleries were emptied during the activation. So to fix this, we had to restore from an old backup
  2. SharePoint 2010 has closed the back door ability to save publishing site as a template.
  3. SharePoint Designer 2010 often poses problems while working with page layouts. This is especially true with connected web parts and few OOB web parts such as the Note Board Web Part (comments web part). In case of connected web parts, SPD interferes and alters the connection IDs to 0000-0000-0000-000. In the case of NoteBoard Web Part, SPD does not let us to shange the title. Resolution for the Note Board Web Part is to change the properties in design mode.
  4. It is not an easy and straight process to set certain properties like Picture URL in User Profile Properties. I have written a blog explaining how to map them to AD correctly Setting up the PictureURL User Profile Property using Forefront Synchronization Service Manager in SharePoint 2010
  5. Forefront Server may cause lot of troubles in case of a single server farm
  6. Content Query Web Part's styles do not work correctly untill they are fixed. Fellow colleague Sandeep Nahta wrote a blog about fixing the issue here
  7. Microsoft has not yet provided options to automatically take backups. We still have to write scripts and use them with a automater like Task Scheduler
  8. The infamous COLD, SLOW requests that baffles users still remains. We have used SPWakeUp with Task Scheduler to overcome this.
  9. If you take a backup of a particular list using the SharePoint 2010 UI's new granular backup interface and restore it to the same location where it has been backed up from, do not expect the list to have the same list ID. It generates a new ID for the list. This can pose a problem especially if this list is being used by a lookup column and if the list gets corrupted for any reason.

--- More to come ---

Making a SharePoint 2010 site externally available (Alternate Access Mappings, Host Header Bindings)

Scenario:
How to make a SharePoint 2010 site externally available?

Explanation:
This is a very fundamental scenario that many SharePoint developers may encounter in the SharePoint world.

In order to make a SharePoint site externally available, we need to follow the below steps

Point the desired host name (i.e. portal.sitename.com for example) to the public ip address of the SharePoint server (may it be the ip of the server in a single server farm or the load balancer in a multi server farm). Typically the network administrator does this. Once that has been set, all the requests for http://portal.sitename.com will be directed to the specified SharePoint Server. But the SharePoint server needs to know which site to serve for such requests. Therefore we need to configure host name bindings in IIS.

Open up IIS Manager and select the desired sharepoint site and choose "Edit Bindings"




In the resulting screen, click edit




Next, provide the host name (portal.sitename.com in this case)



Now its time to configure Alternate Access Mappings in SharePoint.
Go to Central Admin > Application Management > Alternate Access Mappings.


 
Click on "Edit Public URLs" and then choose the appropriate Alternate Access Mapping Collection.



All looks fine now. At this point, if you create a new site collection and try to access the host (http://portal.sitename.com) from a different machine than the server, it works. But if you try to access the host from within the server, you may get a 404 page not found error.

While we are aware of the problems that SharePoint can encounter related to the Windows Server loopback check issue and host headers, I just wanted to reiterate that while it is OK to outright disable the check in dev/qa environments, we should instead be specifying a list of acceptable host names in the registry for production environments.

Completely disabling the check is a security hole that would likely be picked up should one of our production environments be audited.

Both techniques for handling the Loopback check issue are covered in this KB article: http://support.microsoft.com/kb/896861

Thanks a lot to our Practice Lead David Perkinson for helping me resolve this issue.

Specify host names (Preferred method if NTLM authentication is desired)
To specify the host names that are mapped to the loopback address and can connect to Web sites on your computer, follow these steps:
  1. Set the DisableStrictNameChecking registry entry to 1. For more information about how to do this, click the following article number to view the article in the Microsoft Knowledge Base: 281308 (http://support.microsoft.com/kb/281308/ ) Connecting to SMB share on a Windows 2000-based computer or a Windows Server 2003-based computer may not work with an alias name
  2. Click Start, click Run, type regedit, and then click OK.
  3. In Registry Editor, locate and then click the following registry key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0
  4. Right-click MSV1_0, point to New, and then click Multi-String Value.
  5. Type BackConnectionHostNames, and then press ENTER.
  6. Right-click BackConnectionHostNames, and then click Modify.
  7. In the Value data box, type the host name or the host names for the sites that are on the local computer, and then click OK.
  8. Quit Registry Editor, and then restart the IISAdmin service.
Now we should be able to access the site using the host name both internally as well as externally.

Tuesday, May 25, 2010

Setting up the PictureURL User Profile Property using Forefront Synchronization Service Manager in SharePoint 2010

Scenario:

Mapping the User Profile property "PictureURL" is not a straight forward process in SharePoint 2010. In our case, we needed to map this property to the attribute extensionAttribute3.

It is not a straight forward process like the procedure we generally follow to set any other property mapping beacause the attribute "extensionAttribute3" does not exist in the list of mappable attributes. We had to follow the same procedure whenever we could not find the desired attribute in the list of mappable attributes.



Resolution:

In order to setup the mapping, use Forefront Synchronization Service Manager. (It is found at \Program Files\Microsoft Office Servers\14.0\Synchronization Service\UIShell\ miisclient.exe)



Navigate to Metaverse Designer and choose person in the list below:



If extensionAttribute3 is not found in the list of available attributes, click on “Add Attribute” that appears as one of the actions in the right hand pane. If it does not appear in the available attributes list, click the “New Attribute” button.



Make sure that “Indexed” is checked and type in the value “extensionAttribute3” for the Attribute Name.
Now we need to setup the Attribute Flow. In order to do that, navigate to “Management Agents” tab



Choose the desired “Active Directory Domain Service Agent” (MOSSAD-ACTIVE DIRECTORY) and navigate to its properties. Navigate to “Select Attributes” section. If you don’t see extensionAttribute3 in the list, check “Show All” and you will see the extensionAttribute3 in the list. Make sure that it is checked.



Navigate to “Configure Attribute Flow” and setup attribute flow as below:



Now choose the Extensible Connectivity Agent (MOSS-64c01ea7-862d-4c64-969d-8e5e52457cdb)
Navigate to Properties > Configure Attributes. If “PictureURL” is not found in the list of attributes, add it by clicking on “New”.



Navigate to “Define Object Types”. If you don’t see the “PictureURL” attribute in the list, click on “Edit” and then add “PictureURL” from the list of available attributes to the list of “May have attributes”



Navigate to “Configure Attribute Flow” and setup mapping as below:



Now you should be able to see that the Picture attribute is mapped to extensionAttribute3 in the “Edit Profile Property” UI screen.



Thanks a lot to our Practice Lead David Perkinson for all the help he offered me while setting up the search configuration.