Archive

Posts Tagged ‘Share Point’

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.

SharePoint: The usage of SPUtility.GetLocalizedString

May 12th, 2011 No comments

     Sometimes when I parse the Schema.xml of some list I need to resolve such attribute values as:

  • “$Resources:core,lists_Folder;/ONEOFYOURLISTNAME” – often can be found in Lookup-field definitions;
  • “$Resources:groupExpColl;” – can be found in View definition;
  • “$Resources:core,moreItemsParen;” – can be found in View definition;
  • “$ResourcesNoEncode:core,noXinviewofY_LIST;” – can be found in View definition;
  • “$Resources:spscore,FeaturePushdownTaskTitle;”;
  • “$Resources:cmscore,PagesListDisplayName”;
  • and so on.

The method SPUtility.GetLocalizedString(string source, string defaultResourceFile, uint language) is dedicated for retrieving the value for a named resource string from the resource file for a specified language. In example above, a number of strings already contains indications what resource file should be used for searching the localized value, i.e. core, spscore and cmscore. That is why, in most cases, null for defaultResourceFile-parameter can be passed into SPUtility.GetLocalizedString.

If resource file name isn’t included into string (e.g. “$Resources:groupExpColl;”) and defaultResourceFile-parameter is null, I believe, SPUtility.GetLocalizedString searchs in core.resx or in its language-specific variant, e.g. core.en-US.resx (according to language-parameter).

I’ve wrapped the usage of SPUtility.GetLocalizedString into small method, which takes it upon itself to detect the current language.

public static string GetLocalizedString(SPWeb spWeb, string source)
{
    if (string.IsNullOrEmpty(source) || !source.StartsWith("$Resources", StringComparison.OrdinalIgnoreCase))
        return source;
    uint language = spWeb != null ? spWeb.Language : (HttpContext.Current != null ? (uint)Thread.CurrentThread.CurrentUICulture.LCID : 1033);
    return SPUtility.GetLocalizedString(source, null, language);
}

public static string GetLocalizedString(string source)
{
    return GetLocalizedString(null, source);
}

Below the results of GetLocalizedString in respect to each string from the above-mentioned example

String

Localized string

“$Resources:core,lists_Folder;/ONEOFYOURLISTNAME” “Lists/ONEOFYOURLISTNAME”
“$Resources:groupExpColl;” “Expand/Collapse”
“$Resources:core,moreItemsParen;” “(More Items…)”
“$ResourcesNoEncode:core,noXinviewofY_LIST;” “<HTML>There are no items to show in this view of the \”</HTML><ListProperty Select=\”Title\” HTMLEncode=\”TRUE\”><HTML>\” list.</HTML>”
“$Resources:spscore,FeaturePushdownTaskTitle;” “Enable new features on existing sites”
“$Resources:cmscore,PagesListDisplayName” “Pages”
Related posts:

SharePoint: Wrapper over EnsureUser

May 3rd, 2011 No comments

     When I needed to get SPUser-object I always utilized SPWeb.EnsureUser method. EnsureUser looks for the specified user login inside SPWeb.SiteUsers collection, and if the login isn’t found, turns to ActiveDirectory for the purpose of retrieving the user information from there. If such information is found, it will be added to SPWeb.SiteUsers and for the next time it will be returned directly from SPWeb.SiteUsers.

So, we have the fact that when EnsureUser is called, SPWeb-object potentially can be changed (changes affect SPWeb.SiteUsers collection, to be more precise). Each time we change SPWeb-object, it’s highly recommended to make sure that SPWeb.AllowUnsafeUpdates = true. It’s especially topical, when EnsureUser is called from Page during GET-request (Page.IsPostback = false). Otherwise, “Updates are currently disallowed on GET requests. To allow updates on a GET, set the ‘AllowUnsafeUpdates’ property on SPWeb” is thrown.

I’ve implemented some wrapper over the standard SPWeb.EnsureUser:

public static SPUser SafeEnsureUser(SPWeb web, string loginName)
{
    SPUser res = null;
    if (!web.AllowUnsafeUpdates)
    {
        bool oldAllowUnsafeUpdate = web.AllowUnsafeUpdates;
        try
        {
            web.AllowUnsafeUpdates = true;
            res = web.EnsureUser(loginName);
        }
        catch (Exception ex)
        {
            // write to log
        }
        finally
        {
            web.AllowUnsafeUpdates = oldAllowUnsafeUpdate;
        }
    }
    else
        try
        {
            res = web.EnsureUser(loginName);
        }
        catch(Exception ex)
        {
           // write to log
        }

    return res;
}

SafeEnsureUser checks whether web.AllowUnsafeUpdates is true. If yes, it just invoke standard EnsureUser. Otherwise, it preset web.AllowUnsafeUpdates to true and then invoke standard EnsureUser. Nowadays, everywhere I use this method instead of standard one.

If you need an extension for SPWeb-object, try this

public static SPUser SafeEnsureUser(this SPWeb web, string loginName)
{
	// the same method body
}
Related posts:

SharePoint: The specified SPContentDatabase has been upgraded to a newer version

April 6th, 2011 No comments

     Yesterday I was trying to restore the Web-application from fresh SQL-backup of database of production server. Doing that I used the algorithm described in one of my previous post “How to Restore a SharePoint Site from a Database Backup”. I restored backup into a new database, then I tried to create a new Web-application connected to the restored database, however, the undertaking failed, because I received the following error:

The specified SPContentDatabase Name=WSS_CONTENT_PROD Parent=SPDatabaseServiceInstance has been upgraded to a newer version of SharePoint. Please upgrade this SharePoint application server before attempting to access this object.

The specified SPContentDatabase has been upgraded to a newer version of SharePoint. Please upgrade this SharePoint application server before attempting to access this object

     According to the error some update has been deployed on the production server and has led to the version increase. The obvious solution was to upgrade the local environment. First of all, I went to Central Administration->Operations->Servers in Farm and found out the local current version of SharePoint, it was 12.0.0.6504.

Where to take look at version

     In the same time the current version on production was 12.0.0.6510. By means of the page “How To find the SharePoint version” I discovered the cumulative update I should have set up to bring local version to production version. I downloaded the required update and installed. Unfortunately, despite the successful installation, the local version didn’t change at all and the error still remained during a new Web-application creation. When the standard steps don’t help, we need a trick. I tried to find where the restored database contains the information about version. The table Versions struck my eye:

SELECT [VersionId]
      ,[Version]
      ,[Id]
      ,[UserName]
      ,[TimeStamp]
      ,[FinalizeTimeStamp]
      ,[Mode]
      ,[ModeStack]
      ,[Updates]
      ,[Notes]
  FROM [WSS_CONTENT_PROD].[dbo].[Versions]

Table Versions

     As you can see, we have one record where version is greater than local version (12.0.0.6510 > 12.0.0.6504). I replaced 12.0.0.6510 with 12.0.0.6504 using the following sql-command:

Update [WSS_CONTENT_PROD].[dbo].[Versions] 
SET [Version] = '12.0.0.6504' 
where [Version] = '12.0.0.6510'

     After this trick I was able to create a new Web-application connected to the restored database without errors. Of course, Microsoft doesn’t welcome any handmade changes in content database and you should avoid doing them, but if you don’t have a choice you can attempt at your own risk 🙂 In my case it has saved much time and life energy for me 🙂

Related posts:

SharePoint: How to get SystemAccount token

March 25th, 2011 No comments

     As you probably know RunWithElevatedPrivileges allows to run code under the Application Pool identity, which has site collection administrator privileges on all site collections hosted by that application pool. In my opinion RunWithElevatedPrivileges provides too many rights for code, which usually operates in the bounds of only one certain site collection. That is why instead of using RunWithElevatedPrivileges I prefer dealing with an elevated SPSite-object created with SPUserToken of SystemAccount of the target site collection. SystemAccount or SHAREPOINT\system is an alias for the site collection administrators and it has full control over all content in the site collection. We can get it using property SystemAccount.UserToken of SPSite-object. However sometimes the current user doesn’t have permissions to access that property, in this case we will have to use RunWithElevatedPrivileges I’ve criticized above :). The method retrieving the token of SystemAccount can look like the following:

protected static SPUserToken GetSystemToken(SPSite spSite)
{
    SPUserToken res = null;
    bool oldCatchAccessDeniedException = spSite.CatchAccessDeniedException;
    try
    {
        spSite.CatchAccessDeniedException = false;
        res = spSite.SystemAccount.UserToken;
    }
    catch (UnauthorizedAccessException)
    {
        SPSecurity.RunWithElevatedPrivileges(delegate()
        {
            using (SPSite elevatedSPSite = new SPSite(spSite.ID))
                res = elevatedSPSite.SystemAccount.UserToken;      // (***)
        });
    }
    finally
    {
        spSite.CatchAccessDeniedException = oldCatchAccessDeniedException;
    }
    return res;
}

     Here spSite.CatchAccessDeniedException=false allows us to catch and handle UnauthorizedAccessException by ourself, and if the exception happens, we elevates privileges to read the user token by means of RunWithElevatedPrivileges. As we know the most of SharePoint objects somehow refers to SPSite and SPWeb objects, in context of which these SP-objects were created. From this viewpoint the GetSystemToken can look potentially buggy in the line marked with (***), because method returns SPUserToken while its parent elevatedSPSite is being disposed. To justify this code we should take a look at SPUserToken class kindly provided by .Net Reflector:

public sealed class SPUserToken
{
    // Fields
    private byte[] m_token;

    // Methods
    public SPUserToken(byte[] token);
    public bool CompareUser(SPUserToken userTokenCheck);

    // Properties
    public byte[] BinaryToken { get; }
}

     As you can see SPUserToken is just an array of bytes, it doesn’t contain any references to any parental SharePoint objects, therefore we can return it from the method freely. Once SPUserToken is received, it can be cached for a while and reused.

     If you don’t have any original SPSite-object (for example, you have a simple console application) you should create it, e.g.

protected static SPUserToken GetSystemToken(string siteUrl)
{
    SPUserToken res = null;
    using (SPSite spSite = new SPSite(siteUrl))
        res = GetSystemToken(spSite);
    return res;
}

Thanks!

Related posts: