Archive

Archive for the ‘ASP.NET’ Category

SharePoint: Enhanced ItemPicker

February 28th, 2012 No comments

    I was asked to develop a control based on the ItemPicker control, which, in addition to ability of choosing an external data item through BDC, brings it to client side without page postback. There was the following supposed sequence of actions:

  1. An user chooses a data item in the Picker Dialog;
  2. The identifier of the selected item is sent to the server through an Ajax-like technology;
  3. Using the received identifier, the server fetches the proper data out through BDC and sends it back to the client. By “the proper data” I mean the values of either all fields available in the selected data item or only fields defined in control declaration;
  4. On the client side, the received data is parsed and displayed in UI;

Additionally, the control should be free of bindings to SPField as it should be capable to reside within an independent aspx-page locating in the _layout folder.

*Note: for better understanding of BDC infrastructure, please read the following blog posts: SharePoint: Brief introduction to Business Data Catalog and SharePoint: Understanding BusinessData Column.

So, I developed the required control and made it as reusable as possible. Let’s call the control MyItemPicker (it’s so unusual, isn’t? :)). For sake of simplicity I decided to use the ASP.Net client callbacks applied through the ICallbackEventHandler interface. The ASP.Net client callbacks can be considered as a wrapper on XMLHTTP object. Also the MyItemPicker comprises and uses the standard ItemPicker.

Ok, let’s start with declaration of the control within page:

<MYCC:MyItemPicker id="myItemPicker" runat="server" LobSystemInstanceName="Products" 
EntityName="Product" PrimaryColumnName="Name" ClientCallback="MyClientCallback" 
ClientCallbackError="MyClientCallbackError" CallbackBDCFieldFilter="Price,Producer" />

The significant properties here are

  • LobSystemInstanceName is the name of the Lob System Instance, through which data is provided to pick;
  • EntityName is the type name of data items populating the picker;
  • PrimaryColumnName is the name of the data item field, the value of which is used as a display value;
  • ClientCallback is the name of the JavaScript function, which has to be present within the page. In case of success, the given function accepts and processes the server response containing fetched data;
  • ClientCallbackError is the name of the JavaScript function, which can be within the page and is called, when server fails to fulfill request. This property is optional;
  • CallbackBDCFieldFilter is the comma-separated string containing names of data item fields that should be included in server response. For example, if a BDC Entity has four fields – ID, Name, Price and Producer, you might want to have on client side only two of them – Price and Producer. If the CallbackBDCFieldFilter property is empty or not presented in the declaration, server response contains the values of all available fields of BDC Entity;

The sample of the JavaScript functions, which should be indicated in the ClientCallback and ClientCallbackError properties, is shown below. Note the functions’ signatures.

<script type="text/javascript">

    function MyClientCallback(result, context) {

        alert("Result: " + result);
        
        if (result != null && typeof result != "undefined" && result.length != 0) {
            var res = eval("(" + result + ")");

            alert('Price: '    + res.Price);
            alert('Producer: ' + res.Producer);
            
            // update UI with received data
        }        
    }

    function MyClientCallbackError(result, context) {
        alert('Error: ' + result);            
    }

</script>

The server response looks like

{ 'Price' : '10.00', 
  'Producer' : 'Microsoft Corporation' }

Thus, the response is formatted in the manner to be easily turned into JavaScript object by means of the eval function.

So, it’s time to examine the source code of the MyItemPicker itself.

MyItemPicker Source

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Portal.WebControls;
using System.Web.UI;
using Microsoft.Office.Server.ApplicationRegistry.Infrastructure;
using Microsoft.Office.Server.ApplicationRegistry.MetadataModel;
using Microsoft.Office.Server.ApplicationRegistry.Runtime;
using System.Data;

namespace MyControls
{
    public class MyItemPicker : Control, ICallbackEventHandler
    {
        #region fields & properties
        protected ItemPicker _picker                    = null;
        protected string     _callbackRequestedEntityId = string.Empty;

        public string LobSystemInstanceName  { get; set; }
        public string EntityName             { get; set; }
        public string PrimaryColumnName      { get; set; }

        public string ClientCallback         { get; set; }
        public string ClientCallbackError    { get; set; }

        public string CallbackBDCFieldFilter { get; set; }
        #endregion

        #region public methods
        // Implementation of the ICallbackEventHandler interface
        // Generates response that will be sent to client
        public string GetCallbackResult()
        {
            return GetJSResult();
        }
        // Implementation of the ICallbackEventHandler interface
        // Retrieves and preserves identifier of selected data item sent from client
        public void RaiseCallbackEvent(string eventArgument)
        {
            _callbackRequestedEntityId = eventArgument;
        }
        #endregion

        #region internal methods
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            EnsureChildControls();            
        }

        protected override void CreateChildControls()
        {            
            base.CreateChildControls();

            if (_picker == null)
            {
                _picker = new ItemPicker();                
                _picker.MultiSelect = false;
                _picker.ID = ID + "_ItemPicker";
                try
                {
                    this.SetExtendedDataOnPicker(_picker);
                }
                catch (Exception exception)
                {
                    _picker.ErrorMessage = exception.Message;
                    _picker.Enabled = false;
                }

                this.Controls.Add(_picker);
            }            
        }

	/// <summary>
        /// Initilizes main item picker's properties
        /// </summary>        
        protected virtual void SetExtendedDataOnPicker(ItemPicker picker)
        {            
            ItemPickerExtendedData data = new ItemPickerExtendedData();

            BDCMetaRequest request = new BDCMetaRequest(LobSystemInstanceName, EntityName);
            data.SystemInstanceId  = request.FoundLobSystemInstance.Id;
            data.EntityId          = request.FoundEntity.Id;

            List<uint> list = new List<uint>();
            FieldCollection fields = request.FoundEntity.GetSpecificFinderView().Fields;
            foreach (Field field in fields)
                if (string.Equals(field.Name, PrimaryColumnName, StringComparison.OrdinalIgnoreCase))                
                    data.PrimaryColumnId = field.TypeDescriptor.Id;
                else
                    list.Add(field.TypeDescriptor.Id);

            data.SecondaryColumnsIds = list.ToArray();
            picker.ExtendedData = data;
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            AddJSCallbackFunctions();
            AddAdditionalJSFunctions();
        }

        /// <summary>
        /// Generates and adds auxiliary JavaScript functions to the page
        /// </summary> 
        protected void AddAdditionalJSFunctions()
        {
            if (_picker != null)
            {
                _picker.LoadPostData(null, null); // this line is required to force CreateChildControls() and to have HiddenEntityKey created
                Control upLevelDiv = FindControlRecursive(_picker, "upLevelDiv");
                if (upLevelDiv != null)
                {
                    string clearFuncName = "ClearItemPicker_" + ID;
                    string clearFunc =
                        "function " + clearFuncName + "() {" +
                            "var upLevelDiv = document.getElementById('" + upLevelDiv.ClientID + "');" +
                            "if (upLevelDiv != null) {" +
                                "upLevelDiv.innerHTML = '';" +
                                "updateControlValue('" + _picker.ClientID + "');" +
                                "}" +
                            "}";
                    Page.ClientScript.RegisterClientScriptBlock(GetType(), clearFuncName, clearFunc, true);
                }
                
                Control hiddenEntityDisplayTextControl = FindControlRecursive(_picker, "HiddenEntityDisplayText");
                if (hiddenEntityDisplayTextControl != null)
                {
                    string getDisplayTextFuncName = "GetDisplayText_" + ID;
                    string getDisplayTextFunc =
                        "function " + getDisplayTextFuncName + "() {" +
                            "var hiddenEntityDisplayTextControl = document.getElementById('" + hiddenEntityDisplayTextControl.ClientID + "');" +
                            "return hiddenEntityDisplayTextControl != null ? hiddenEntityDisplayTextControl.value : '';" +
                        "}";
                    Page.ClientScript.RegisterClientScriptBlock(GetType(), getDisplayTextFuncName, getDisplayTextFunc, true);
                }
            }            
        }

        /// <summary>
        /// Generates and adds the picker's AfterCallbackClientScript to the page
        /// </summary> 
        protected void AddJSCallbackFunctions()
        {
            if (_picker != null)
            {
                string callbackFunc = null;
                if (!string.IsNullOrEmpty(ClientCallback) && !string.IsNullOrEmpty(ClientCallbackError))
                    callbackFunc = Page.ClientScript.GetCallbackEventReference(this, "arg", ClientCallback, "context", ClientCallbackError, true);
                else
                {
                    if (!string.IsNullOrEmpty(ClientCallback))
                        callbackFunc = Page.ClientScript.GetCallbackEventReference(this, "arg", ClientCallback, "context", true);
                }
                if (!string.IsNullOrEmpty(callbackFunc))
                {
                    _picker.LoadPostData(null, null); // this line is required to force CreateChildControls() and to have HiddenEntityKey created

                    Control pickerEntityKeyHidden = FindControlRecursive(_picker, "HiddenEntityKey");
                    if (pickerEntityKeyHidden != null)
                    {
                        string clientFuncName = "GetBdcFieldValuesAsync_" + ID;
                        string clientFunc =
                            "function " + clientFuncName + "(context)" +
                            "{" +
                                "var pickerEntityKeyHidden = document.getElementById('" + pickerEntityKeyHidden.ClientID + "');" +
                                "if (pickerEntityKeyHidden != null)" +
                                "{" +
                                    "var arg = pickerEntityKeyHidden.value;" +
                                    callbackFunc + ";" +
                                "}" +
                            "}";
                        Page.ClientScript.RegisterClientScriptBlock(GetType(), clientFuncName, clientFunc, true);
                        _picker.AfterCallbackClientScript = clientFuncName + "('" + ID + "');";
                    }
                }
            }
        }

        /// <summary>
        /// Makes request to external data source and returns json-result
        /// </summary>         
        protected string GetJSResult()
        {
            string res = string.Empty;

            try
            {
                if (!string.IsNullOrEmpty(_callbackRequestedEntityId))
                {
                    Dictionary<string, byte> bdcFieldFilter = GetBDCFieldFilter(CallbackBDCFieldFilter);
                    
                    BDCRequestById request = new BDCRequestById(LobSystemInstanceName, EntityName, _callbackRequestedEntityId);

                    StringBuilder sb = new StringBuilder();
                    sb.Append("{");
                    foreach (Field field in request.FoundEntityInstance.ViewDefinition.Fields)
                    {
                        if (bdcFieldFilter.Count == 0 || bdcFieldFilter.ContainsKey(field.Name))
                        {
                            if (sb.Length > 1)
                                sb.Append(", ").AppendLine();
                            sb.Append("'").Append(field.Name).Append("' : ");
                            sb.Append("'").Append(Convert.ToString(request.FoundEntityInstance.GetFormatted(field))).Append("'");
                        }
                    }
                    sb.Append("}");                    

                    res = sb.ToString();
                }
            }
            catch (Exception ex)
            {
                // write error to log
            }

            return res;
        }

        /// <summary>
        /// Parses the user defined list of bdc fields, the values of which should be retrieved
        /// </summary>        
        protected static Dictionary<string, byte> GetBDCFieldFilter(string commaSeparatedBdcFields)
        {
            Dictionary<string, byte> res = new Dictionary<string, byte>(StringComparer.OrdinalIgnoreCase);

            if (!string.IsNullOrEmpty(commaSeparatedBdcFields))
            {
                string[] bdcFields = commaSeparatedBdcFields.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string field in bdcFields)
                    res.Add(field, 0);
            }

            return res;
        }
        #endregion
    }
}

You probably noticed that the functions AddJSCallbackFunctions and AddAdditionalJSFunctions generate and add some JavaScript functions to the page. The exact names of these JavaScript functions depend on the id attribute defined in control declaration. For example, if control id is “myItemPicker“, the functions’ name will be GetBdcFieldValuesAsync_myItemPicker, ClearItemPicker_myItemPicker and GetDisplayText_myItemPicker.

Let’s take a look at the functions. The main function is GetBdcFieldValuesAsync_myItemPicker, which extracts the encoded id of selected item from ItemPicker and then makes the Ajax-like client callback to the server. The rest two functions are auxiliary, they are not used by MyItemPicker directly, but they are very useful for developing an interaction between user and MyItemPicker. As their names imply, the ClearItemPicker_myItemPicker clears the ItemPicker, and the GetDisplayText_myItemPicker returns the text displayed to user in ItemPicker. The listing below demonstrates the functions within page:

<script type="text/javascript">

    function GetBdcFieldValuesAsync_myItemPicker(context) {
        var pickerEntityKeyHidden = document.getElementById('myItemPicker_ItemPicker_HiddenEntityKey');
        if (pickerEntityKeyHidden != null) {
            var arg = pickerEntityKeyHidden.value;
            WebForm_DoCallback('myItemPicker', arg, ClientCallback, context, ClientCallbackError, true);
        }
    }

    function ClearItemPicker_myItemPicker() {
        var upLevelDiv = document.getElementById('myItemPicker_ItemPicker_upLevelDiv');
        if (upLevelDiv != null) {
            upLevelDiv.innerHTML = '';
            updateControlValue('myItemPicker_ItemPicker');
        }
    }

    function GetDisplayText_myItemPicker() {
        var hiddenEntityDisplayTextControl = document.getElementById('myItemPicker_ItemPicker_HiddenEntityDisplayText');
        return hiddenEntityDisplayTextControl != null ? hiddenEntityDisplayTextControl.value : '';
    }
</script>

The SetExtendedDataOnPicker and GetJSResult methods of MyItemPicker employ the classes BDCMetaRequest and BDCRequestById that are described in my post SharePoint: How to get value from BDC.

The FindControlRecursive method is mentioned in another my post.

SharePoint: How to make error messages more detailed

December 21st, 2011 No comments

    If an unexpected error occurs on a SharePoint environment, by default, you get a meaningless error message as the following: An unexpected error has occurred. Despite the given error message is considered as friendly one for users, we as developers want to have more information to detect the reason. SharePoint is built on ASP.Net technology, it means we can enable the detailed error message and, in addition, displaying of call-stack by changing some settings in a web.config file. We need to find the node customErrors and change its mode to “Off”, which specifies that custom errors are disabled and the detailed errors are shown to the local and remote clients. Then we need to find the node SafeMode and change its CallStack to “true”. CallStack attribute defines whether a call-stack and an exception message are displayed when a system-level exception takes place while ASP.NET processes a request from the local and remote clients. The last step is to save the made changes. The nodes we are interested in are shown below:

Before:

<customErrors mode="On" />
<SafeMode MaxControls="200" CallStack="false" ... >

After:

<customErrors mode="Off" />
<SafeMode MaxControls="200" CallStack="true" ... >

The required web.config file containing main settings locates in \inetpub\wwwroot\wss\VirtualDirectories\<SHAREPOINT APP PORT NUMBER>. For SharePoint 2007 it’s usually enough to make changes in the given file only. As for SharePoint 2010, the changes also should be applied to a number of additional web.config files in 14 hive (\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14) of SharePoint 2010. If you make a search for web.config inside the 14 folder you likely will find more than 30 different files. I recommend firstly to make changes in web.config files locating in \14\TEMPLATE\ADMIN and \14\TEMPLATE\LAYOUTS and then check whether you still get not detailed error message. If so, continue making the changes file by file until the message will get more detailed.

Related posts:

SharePoint: How to find all controls of a certain type

October 12th, 2011 No comments

    A small, but useful method to find recursively all controls of a certain type:

public static List<T> FindControlRecursiveByType<T>(Control root) where T : Control
{
    List<T> res = new List<T>();

    if (root != null)
    {
        Stack<Control> tmpStack = new Stack<Control>();
        tmpStack.Push(root);

        while (tmpStack.Count > 0)
        {
            Control ctrl = tmpStack.Pop();
            if (ctrl is T)
                res.Add(ctrl as T);

            foreach (Control childCtrl in ctrl.Controls)
                tmpStack.Push(childCtrl);
        }
    }

    return res;
}

The sample usage is below:

// return all save buttons on the page
List<SaveButton> saveButtons = FindControlRecursiveByType<SaveButton>(Page);
Related posts:
Categories: ASP.NET, Share Point Tags: ,

SharePoint: How to use SPLongOperation

August 11th, 2011 No comments

     If you need to run a lengthy server operation, it’s reasonable to use SPLongOperation. SPLongOperation shows a spinning wheel indicator with a specified text on the web page during a lengthy operation. You have probably seen SPLongOperation at work while creating new web applications or site collection using SharePoint Central Administration.

SPLongOperation - spin wheel with associated text

To use SPLongOperation, you need to create a new object of the SPLongOperation type, set the LeadingHTML and TrailingHTML properties to tell user what’s going on at the moment and call the SPLongOperation.Begin method to start the process. Then run the code of the lengthy server operation. Once the operation is done, call the SPLongOperation.End method passing the URL where user will be redirected to. Here is a typical code that utilizes SPLongOperation on a web page is:

void startLongOperationButton_Click(object sender, EventArgs e)
{
    using (SPLongOperation operation = new SPLongOperation(this.Page))
    {

        operation.LeadingHTML  = "Long operation";
        operation.TrailingHTML = "Please wait while the long operation is in progress";

        operation.Begin();

        // the code of the lengthy operation

        // redirecting to a page that, for example, informs user that the long operation has successfully completed
        operation.End("SomePage.aspx");
    }
}

I use the SPLongOperation, for example, when I need to create a list item with a huge number of unique permissions. It can take dozens of seconds, and it’s a good practice to use the spinning wheel indicator with a descriptive text to tell an impatient user that the application is in progress and not hung on him.

I’ve wrapped interaction with SPLongOperation into the following class and method:

/// <summary>
/// Represents all settings that can be specified to use SPLongOperation
/// </summary> 
public class SPLongOperationExecutionParams
{
    public string LeadingHtml  { get; set; }
    public string TrailingHtml { get; set; }
    public string RedirectUrl  { get; set; }
}

/// <summary>
/// Invokes an action inside SPLongOperation
/// </summary>
/// <param name="executionParams">Settings to use SPLongOperation</param>
/// <param name="action">User specified action</param>
public static void DoInSPLongOperationContext(SPLongOperationExecutionParams executionParams, Action action)
{
    if (HttpContext.Current.CurrentHandler is Page)
    {
        using (SPLongOperation operation = new SPLongOperation(HttpContext.Current.CurrentHandler as Page))
        {
            operation.LeadingHTML  = !string.IsNullOrEmpty(executionParams.LeadingHtml)  ? executionParams.LeadingHtml  : "Long operation";
            operation.TrailingHTML = !string.IsNullOrEmpty(executionParams.TrailingHtml) ? executionParams.TrailingHtml : "Please wait while the long operation is in progress";

            operation.Begin();

            if (action != null)
                action();

            try
            {
                operation.End(executionParams.RedirectUrl, SPRedirectFlags.Trusted, HttpContext.Current, "");
            }
            catch (System.Threading.ThreadAbortException)
            {
                // This exception is thrown because the SPLongOperation.End 
                // calls a Response.End internally
            }
        }
    }
    else
        throw new ApplicationException("Couldn't find a host page!");
}

Here is an example of how to use the wrapper:

void startLongOperationButton_Click(object sender, EventArgs e)
{
    SPLongOperationExecutionParams spLongOperationExecutionParams = new SPLongOperationExecutionParams() { LeadingHtml = "Creation of a new list item", TrailingHtml = "Please wait while the item is being created", RedirectUrl = "SomePage.aspx" };
    DoInSPLongOperationContext(spLongOperationExecutionParams, delegate()
    {
        // the code of the lengthy operation
        Thread.Sleep(5000);
    });
}

As you have probably noticed, I call the SPLongOperation.End inside the try…catch. I do so because this method may cause ThreadAbortException. The SPLongOperation.End internally calls Response.End, which stops execution of the page by means of the Thread.CurrentThread.Abort method. Here is the code of the Response.End method extracted by Reflector:

public void End()
{
    if (this._context.IsInCancellablePeriod)
    {
        InternalSecurityPermissions.ControlThread.Assert();
        Thread.CurrentThread.Abort(new HttpApplication.CancelModuleException(false)); (*)
    }
    else if (!this._flushing)
    {
        this.Flush();
        this._ended = true;
        if (this._context.ApplicationInstance != null)
        {
            this._context.ApplicationInstance.CompleteRequest();
        }
    }
}

The Thread.Abort method raises ThreadAbortException in the thread where it is invoked to begin the process of terminating the thread. I catch those exceptions and just do nothing about them.

In some blogs, people tell that SPLongOperation.End calls Response.Redirect internally, but Reflector shows that it’s not so in the reality. SPLongOperation.End calls Response.End directly. The redirection is accomplished through a special JavaScript, which is added to the web page right after SPLongOperation.End is invoked. I emphasize this fact, because you may attempt to pass the SPRedirectFlags.DoNotEndResponse flag to the SPLongOperation.End method. This flag makes sense only when we use the Response.Redirect, because only in that case Response.End wouldn’t be invoked, and, consequently, ThreadAbortException wouldn’t be thrown. But unfortunately, as I said above, SPLongOperation.End doesn’t use Response.Redirect and, therefore, SPRedirectFlags.DoNotEndResponse will be ignored, and the exception will be thrown.

Please also note that SPLongOperation keeps connection between client and server alive and the Response stream open. Therefore, if your long operation takes longer than the value of httpRuntime.executionTimeout defined in Web.config (or Machine.config) you will receive the ‘Request Timed Out’ exception. Therefore, to perform a really long operation (for example, longer than the default time-out value, which is 90 seconds), you would have to refuse SPLongOperation and look for some other approach.

SharePoint: How to customize Content breadcrumb navigation

August 3rd, 2011 No comments

     Recently, I’ve stumbled over a problem with a Content breadcrumb navigation. Content breadcrumb navigation is a set of hyperlinks that enables site users to quickly navigate up the hierarchy of sites within a site collection.

Content breadcrumb navigation

The problem was that in the edit mode the link with the item title (for example, like the Test post on image) sometimes had a wrong url. Content breadcrumb navigation is a SiteMapPath control, which is rendered based upon site map nodes, provided by a SiteMapProvider. An obvious way to solve the issue is to find the problematic site map node in the tree and try to modify the node in the memory, i.e. to replace its url or the entire node.

I have got a fully customized edit page for my list (for each content type, actually); in other words, I got access to the executable code (so-called, code-behind) and the markup of the aspx-page. That is the main requirement for the successful resolution of the issue.

First of all, I found a place where SiteMapPath had been declared in default.master:

<asp:ContentPlaceHolder id="PlaceHolderTitleBreadcrumb" runat="server">
    <asp:SiteMapPath SiteMapProvider="SPContentMapProvider" id="ContentMap" SkipLinkText="" NodeStyle-CssClass="ms-sitemapdirectional" runat="server"/> 

&nbsp;
</asp:ContentPlaceHolder>

Then I copied it to my edit aspx-page with the appropriate changes:

<asp:Content ID="ContentBreadcrumb" ContentPlaceHolderID="PlaceHolderTitleBreadcrumb" runat="server">    
    <asp:SiteMapPath SiteMapProvider="SPContentMapProvider" id="SiteMapPathContentMap" SkipLinkText="" NodeStyle-CssClass="ms-sitemapdirectional" 

runat="server" /> &nbsp;
</asp:Content>

From that point, I could easily manipulate the SiteMapPath control in the code-behind of aspx-page.

The standard way to programmatically modify site map nodes in the memory is to hande the SiteMap.SiteMapResolveevent on an ASP.Net web page. This way is described in msdn http://msdn.microsoft.com/en-us/library/ms178425%28v=VS.85%29.aspx. The following listing demonstrates how to connect to the required SiteMapResolve event. Note that our target SiteMapProvider is SPContentMapProvider.

protected void Page_Load(object sender, EventArgs e)
{            
    Microsoft.SharePoint.Navigation.SPContentMapProvider prov = 

(Microsoft.SharePoint.Navigation.SPContentMapProvider)SiteMap.Providers["SPContentMapProvider"];
    if (prov != null)
        prov.SiteMapResolve += new SiteMapResolveEventHandler(prov_SiteMapResolve);
}

In the prov_SiteMapResolve handler, I tried using two variants of code. The first variant is to set the required url to a certain SiteMapNode, which turns into a link with the item title:

SiteMapNode prov_SiteMapResolve(object sender, SiteMapResolveEventArgs e)
{
    SiteMapNode currentNode = e.Provider.CurrentNode; // in our case current node is 'Edit Item'
    SiteMapNode parentNode  = currentNode.ParentNode; // parent node is 'Test post'

    if (parentNode != null && parentNode.Title.Equals(SPContext.Current.ListItem.Title, StringComparison.OrdinalIgnoreCase))
    {
        bool originalReadOnly = parentNode.ReadOnly;
        parentNode.ReadOnly   = false;
        parentNode.Url        = "microsoft.com";
        parentNode.ReadOnly   = originalReadOnly;
    }
    return currentNode;
}

This variant passes through without errors, but at the same time produces no result. The problematic hyperlink remains with a wrong url.

Another variant tries to replace current node with a new SiteMapNode object:

SiteMapNode prov_SiteMapResolve(object sender, SiteMapResolveEventArgs e)
{
    SiteMapNode currentNode = e.Provider.CurrentNode; // in our case current node is 'Edit Item'
    SiteMapNode parentNode  = currentNode.ParentNode; // parent node is 'Test post'

    if (parentNode != null && parentNode.Title.Equals(SPContext.Current.ListItem.Title, StringComparison.OrdinalIgnoreCase))
    {
	// Clone the current node and all of its relevant parents. This
        // returns a site map node that can then be walked and modified.
        // Since the cloned nodes are separate from the underlying
        // site navigation structure, the changes that are made do not
        // effect the overall site navigation structure.
        currentNode    = currentNode.Clone(true);
        parentNode     = currentNode.ParentNode;
        parentNode.Url = "microsoft.com";
    }
    return currentNode;
}

This code throws an exception – “Unable to cast object of type ‘System.Web.SiteMapNode’ to type ‘Microsoft.SharePoint.Navigation.SPSiteMapNode’.” Which is quite expected, as SPContentMapProvider indeed provides a tree of SPSiteMapNodes and not SiteMapNodes. Moreover, there is no way to go around this obstacle, as SPSiteMapNode is an internal class, so we cannot create instances of it. So, I must conclude that the use of the SiteMapResolve event, which successfully works for ASP.Net, doesn’t work for SharePoint.

But let’s try a different way. The SiteMapPath control has these two events: ItemCreated and ItemDataBound. Theoretically, they could be used for changing the url of nodes. I’ve tried using them both as follows:

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);

    SiteMapPathContentMap.ItemCreated += new SiteMapNodeItemEventHandler(SiteMapPathContentMap_ItemCreated);
    //SiteMapPathContentMap.ItemDataBound += new SiteMapNodeItemEventHandler(SiteMapPathContentMap_ItemDataBound);
}
void SiteMapPathContentMap_ItemCreated(object sender, SiteMapNodeItemEventArgs e)
{
    if (e.Item.ItemType == SiteMapNodeItemType.PathSeparator)
        return;

    if (e.Item.SiteMapNode.Title.Equals(SPContext.Current.ListItem.Title, StringComparison.OrdinalIgnoreCase))
    {
        bool originalReadOnly       = e.Item.SiteMapNode.ReadOnly;
        e.Item.SiteMapNode.ReadOnly = false;
        e.Item.SiteMapNode.Url      = "microsoft.com";
        e.Item.SiteMapNode.ReadOnly = originalReadOnly;
    }
}

Unfortunately, that produces no result too. e.Item.SiteMapNode.Url does change indeed, but the rendered hyperlink still remains with a wrong url. What could cause that? Let’s take a look inside SiteMapPath by means of Reflector. SiteMapPath contains the CreateItem method, which is called for each site map node, and which creates SiteMapNodeItem.

private SiteMapNodeItem CreateItem(int itemIndex, SiteMapNodeItemType itemType, SiteMapNode node)
{
    SiteMapNodeItem item = new SiteMapNodeItem(itemIndex, itemType);
    int index = (this.PathDirection == PathDirection.CurrentToRoot) ? 0 : -1;
    SiteMapNodeItemEventArgs e = new SiteMapNodeItemEventArgs(item);
    item.SiteMapNode = node;
    this.InitializeItem(item);
    this.OnItemCreated(e);
    this.Controls.AddAt(index, item);
    item.DataBind();
    this.OnItemDataBound(e);
    item.SiteMapNode = null;
    item.EnableViewState = false;
    return item;
}

The ItemCreated and ItemDataBound events are fired inside this method. Note that ItemCreated activates immediately after calling InitializeItem. Let’s take a look at InitializeItem.

protected virtual void InitializeItem(SiteMapNodeItem item)
{
    // some code is skipped

    SiteMapNode siteMapNode = item.SiteMapNode;

    // some code is skipped


    if (itemType == SiteMapNodeItemType.PathSeparator)
    {
        Literal child = new Literal
        {
            Mode = LiteralMode.Encode,
            Text = this.PathSeparator
        };
        item.Controls.Add(child);
        item.ApplyStyle(s);
    }
    else if ((itemType == SiteMapNodeItemType.Current) && !this.RenderCurrentNodeAsLink)
    {
        Literal literal2 = new Literal
        {
            Mode = LiteralMode.Encode,
            Text = siteMapNode.Title
        };
        item.Controls.Add(literal2);
        item.ApplyStyle(s);
    }
    else
    {
        HyperLink link = new HyperLink(); // (*)
        if ((s != null) && s.IsSet(0x2000))
        {
            link.Font.Underline = s.Font.Underline;
        }
        link.EnableTheming = false;
        link.Enabled = this.Enabled;
        if (siteMapNode.Url.StartsWith(@"\\", StringComparison.Ordinal))
        {
            link.NavigateUrl = base.ResolveClientUrl(HttpUtility.UrlPathEncode(siteMapNode.Url));
        }
        else
        {
            link.NavigateUrl = (this.Context != null) ? 
this.Context.Response.ApplyAppPathModifier(base.ResolveClientUrl(HttpUtility.UrlPathEncode(siteMapNode.Url))) : siteMapNode.Url;
        }
        link.Text = HttpUtility.HtmlEncode(siteMapNode.Title);
        if (this.ShowToolTips)
        {
            link.ToolTip = siteMapNode.Description;
        }
        item.Controls.Add(link);
        link.ApplyStyle(s);
    }
}

As we can see, InitializeItem creates and fills out a HyperLink object (see the line marked as (*)) that corresponds to the passed SiteMapNodeItem and, consequently, to the linked SiteMapNode. After the creation and initialization, the HyperLink object is added to the control tree of SiteMapNodeItem. Thereby, the ItemCreated event is fired immediately after all the required controls, including hyperlinks, are already created and added to the control tree. That means that all our changes of SiteMapNode inside the ItemCreated handler are doomed to have no effect, just because by that time all the hyperlinks are already created and are not going to be modified.

How to get over this problem? I’ve created an enhanced version of the SiteMapPath control. This derived class has an ItemCreating event, which is fired immediately after SiteMapNodeItem is created but before InitializeItem is called. Here is the control:

public class MySiteMapPath : SiteMapPath
{
    private static readonly object _eventItemCreating = new object();

    public event SiteMapNodeItemEventHandler ItemCreating
    {
        add    { Events.AddHandler(_eventItemCreating, value); }
        remove { Events.RemoveHandler(_eventItemCreating, value); }
    }

    protected virtual void OnItemCreating(SiteMapNodeItemEventArgs e)
    {
        SiteMapNodeItemEventHandler handler = (SiteMapNodeItemEventHandler)base.Events[_eventItemCreating];
        if (handler != null)
            handler(this, e);
    }

    protected override void InitializeItem(SiteMapNodeItem item)
    {
        OnItemCreating(new SiteMapNodeItemEventArgs(item));
        base.InitializeItem(item);
    }
}

In the handler of ItemCreating event, we can freely change site map nodes, and the hyperlinks will reflect those changes. Here is an example of the use of the MySiteMapPath in code-behind:

public MySiteMapPath SiteMapPathContentMap;

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);

    if(SiteMapPathContentMap != null)
        SiteMapPathContentMap.ItemCreating += new SiteMapNodeItemEventHandler(SiteMapPathContentMap_ItemCreating);
}

void SiteMapPathContentMap_ItemCreating(object sender, SiteMapNodeItemEventArgs e)
{
    if (e.Item.ItemType == SiteMapNodeItemType.PathSeparator)
        return;

    if (e.Item.SiteMapNode.Title.Equals(SPContext.Current.ListItem.Title, StringComparison.OrdinalIgnoreCase))
    {
        bool originalReadOnly       = e.Item.SiteMapNode.ReadOnly;
        e.Item.SiteMapNode.ReadOnly = false;
        e.Item.SiteMapNode.Url      = "microsoft.com";
        e.Item.SiteMapNode.ReadOnly = originalReadOnly;
    }	    
}

This is the murkup that has to be in aspx-page:

<asp:Content ID="ContentBreadcrumb" ContentPlaceHolderID="PlaceHolderTitleBreadcrumb" runat="server">    
    <myNamespace:MySiteMapPath SiteMapProvider="SPContentMapProvider" id="SiteMapPathContentMap" SkipLinkText="" 

NodeStyle-CssClass="ms-sitemapdirectional" runat="server" /> &nbsp;
</asp:Content>

So, if you want to change certain properties of a content breadcrumb navigation hyperlink, just declare MySiteMapPath and handle its ItemCreating event in the appropriate way.