Archive

Archive for the ‘Object Model’ Category

SharePoint: How to add a new SPGroup through the code

March 9th, 2014 No comments

    The below listed CreateSpGroup methods along with a few subsidiary ones could be used to add a new SPGroup to a SPWeb

public static SPGroup CreateSpGroup(SPWeb spWeb, string groupName, 
                                    string description, bool isAssociated)
{
  SPGroup res = GetGroupByName(spWeb, groupName);
  if (res == null)
  {
    spWeb.SiteGroups.Add(groupName, spWeb.Site.SystemAccount, null, description);
    res = spWeb.SiteGroups[groupName];

    if (isAssociated)
    {
      spWeb.AssociatedGroups.Add(res);

      const string createdGroupsProperty = "vti_createdassociategroups";
      var createdAssociatedGroupsStr = spWeb.AllProperties[createdGroupsProperty] as string;
      if (!string.IsNullOrEmpty(createdAssociatedGroupsStr))
      {
        var createdAssociatedGroups = ParseAssociateGroupsFromString(createdAssociatedGroupsStr);
        if (!createdAssociatedGroups.Contains(res.ID))
        {
          createdAssociatedGroups.Add(res.ID);
          spWeb.AllProperties[createdGroupsProperty] = 
                               ConvertAssociateGroupsToString(createdAssociatedGroups);
        }
      }
      spWeb.Update();
    }
  }
  return res;
}

public static SPGroup CreateSpGroup(SPWeb spWeb, string groupName)
{
	return CreateSpGroup(spWeb, groupName, string.Empty, false);
}

public static List<int> ParseAssociateGroupsFromString(string str)
{
  var list = new List<int>();
  if (!string.IsNullOrEmpty(str))
    foreach (string str2 in str.Split(new[] { ';' }))
    {
        int num2;
        if ((!string.IsNullOrEmpty(str2) && 
            int.TryParse(str2, NumberStyles.Integer, CultureInfo.InvariantCulture, out num2)) && 
                    ((num2 > 0) && !list.Contains(num2)))
          list.Add(num2);
    }
  return list;
}

public static string ConvertAssociateGroupsToString(List<int> groupIds)
{
	var builder = new StringBuilder();
	for (int i = 0; i < groupIds.Count; i++)
	{
		if (builder.Length > 0)
			builder.Append(';');
		builder.Append(groupIds[i].ToString(CultureInfo.InvariantCulture));
	}
	return builder.ToString();
}

public static SPGroup GetGroupByName(SPWeb spWeb, string groupName)
{
	foreach (SPGroup group in spWeb.SiteGroups)
		if (groupName.Equals(group.Name, StringComparison.OrdinalIgnoreCase))
			return group;
	return null;
}

At first the CreateSpGroup method makes sure whether the target group doesn’t exist by calling the GetGroupByName. If such group has been found, it’s returned, otherwise a new empty group is being created. The SPWeb.Site.SystemAccount aka Site Collection administrator will be the owner of the new group.

The isAssociated flag indicates if the group has to be associated with the site. Let’s linger on associated groups a bit longer.

When a site (or sub-site with unique permissions) is created, three default groups associated with the site become available. Their names usually are “YourSiteName Owners”, “YourSiteName Members” and “YourSiteName Visitors”, where the YourSiteName is the name you gave to your site. You can see them in the Groups section at Quick Launch area when going to the “People and Groups” page (Site Actions -> Site Settings -> People and groups). Through the code those groups are accessible as SPWeb.AssociatedOwnerGroup, SPWeb.AssociatedMemberGroup and SPWeb.AssociatedVisitorGroup respectively. We also can get their IDs from the AllProperties collection of a SPWeb instance like SPWeb.AllProperties[“vti_associateownergroup”], SPWeb.AllProperties[“vti_associatemembergroup”] and SPWeb.AllProperties[“vti_associatevisitorgroup”] respectively. We are able to associate our own custom groups with the site by adding them to the SPWeb.AssociatedGroups collection, so that our groups would appear in the Quick Launch at the “People and Groups” page. So, that’s the first thing the CreateSpGroup does in case the isAssociated is set to true.

One more often-mentioned feature of associated groups is that they allegedly can be deleted automatically when the site/sub-site they are associated with is being deleted. Here, however, there is a pitfall: all groups are SiteCollection-level objects, therefore removing the site/sub-site, the groups (including associated ones) are still available as they are linked to the site collection. SharePoint, however, treats the associated default groups in a special way, namely the groups specified in the SPWeb.AssociatedOwnerGroup, SPWeb.AssociatedMemberGroup and SPWeb.AssociatedVisitorGroup will be actually deleted with the site/sub-site jointly. As regards custom associated groups, in the general case, they will never be automatically deleted with the site. So, don’t count on it. If, however, you are still eager to make your custom group deletable by default, you don’t have any choice but to override one of the associated default groups with the custom one. That’s not a complete solution though, you also have to add the id of the custom group to the spWeb.AllProperties[“vti_createdassociategroups”]. I came to this, having reviewed such methods as Microsoft.SharePoint.Utilities.SPUtilityInternal.CreateDefaultSharePointGroups and Microsoft.SharePoint.ApplicationPages.DeleteWebPage.OnLoad by means of .Net Reflector. So, in case the isAssociated is set to true, the second thing the CreateSpGroup does is adding group’s id to the “vti_createdassociategroups” property. After that the only step you have to do to make your custom group deletable is something like this

spWeb.AssociatedVisitorGroup = CreateSpGroup(spWeb, "my own custom group", string.Empty, true);
//spWeb.AssociatedMemberGroup  = CreateSpGroup(spWeb, "my own custom group", string.Empty, true);
//spWeb.AssociatedOwnerGroup   = CreateSpGroup(spWeb, "my own custom group", string.Empty, true);

Note that, doing that, the overridden default group still remains and is linked to the Site Collection. In case it is not needed any more, remove it. So, before overriding the default group, you’d better save the reference to it and then, after overriding, remove it by calling SPWeb.SiteGroups.Remove.

Nevertheless I suggest you not override associated default groups as you are always able to customize them the way you need instead: change name and description, add users you want and so on.

One more important thing to be aware of is, deleting a site/sub-site, you likely just move it to the Site Collection Recycle Bin (unless the Recycle Bin is off). That means the associated default groups will not be deleted right away, they will be deleted only at that moment when the site is being erased from the Recycle Bin. Unlike the site they are associated with, the groups will not be moved to the Recycle Bin. Moreover SharePoint doesn’t even mark them as “being deleted”. Thus during the Recycle Bin retention period (30 days by default) the associated default groups, being the SiteCollection-level objects, remain available and can be used by someone else. So, somebody who rashly uses the default groups of the “deleted” site may be unpleasantly surprised some day as the groups suddenly disappear. To avoid this, I personally (of course if I have no doubts that the site/sub-site has to vanish completely) erase it from Recycle Bin right away or ask Site Collection administrators to do this as soon as possible.

Let’s go back to the rest of the methods listed above. The ParseAssociateGroupsFromString method parses the value of vti_createdassociategroups into an array of groups’ IDs. The ConvertAssociateGroupsToString, in turn, makes inverse operation and turns the array into a string to save it in the vti_createdassociategroups property. The format of the vti_createdassociategroups is very simple, it’s a set of groups’ IDs separated with semicolons like 1;2;3. Nothing is easier than to implement parse of such string. I, however, decided to use the methods found in SharePoint‘s interior. So, ParseAssociateGroupsFromString and ConvertAssociateGroupsToString are internal methods defined in the Microsoft.SharePoint.ApplicationPages.PeopleGroupPageBase and captured by .Net Reflector.

The GetGroupByName is intended to look for a group with a specified name and used to check if the group about to be created exists.

As I said before, the groups are SiteCollection-level objects, so groups have to be created under privileged account at least under Site Collection administrator. So, the CreateSpGroup could be called as shown below:

// use of system account
string siteUrl = "some site url";
SPUserToken spUserToken = GetSystemToken(siteUrl);
using (SPSite spSite = new SPSite(siteUrl, spUserToken))
{
	using (SPWeb spWeb = spSite.OpenWeb())
	{
		SPGroup newGroup = CreateSpGroup(spWeb, "new group", string.Empty, true);
		// do something with the new group
	}
}

*Note: the GetSystemToken method is described here – SharePoint: How to get SystemAccount token.

or

// use of application pool identity
SPSecurity.RunWithElevatedPrivileges(delegate
{
    using (SPSite spSite = new SPSite("some site url"))
        using (SPWeb spWeb = spSite.OpenWeb())
        {
            SPGroup newGroup = CreateSpGroup(spWeb, "new group", string.Empty, true);
            // do something with the new group
        }
});

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

July 25th, 2013 No comments

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

Add Heading to Quick Launch

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

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

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

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

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

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

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

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

	headingNode.Update();

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

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

SharePoint: HttpContext.Current is null in event receivers

July 12th, 2013 No comments

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

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

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

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

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

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

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

SharePoint: How to Delete a List Field/Column programmatically

July 13th, 2012 No comments

    Sometimes I need to remove a list field, which is not in use anymore. To remove the field we need just to call SPField.Delete method. However, there are situations when the field cannot be deleted due to some conditions, for example, when the field is read-only and etc. Because of that you might get such exceptions as

"The field cannot be deleted because it is a read only field in the list."
"The field cannot be deleted because it is a sealed field in the list."
"The field cannot be deleted because it is a hidden field in the list."

and other. So, let’s consider how these difficulties can be overcome (if it’s possible at all).

Inside the SPField.Delete method

The SPField.Delete does nothing except calling the Delete method of the SPFieldCollection class. The listing below demonstrates the short version of the SPFieldCollection.Delete:

public void Delete(string internalFieldName)
{
    SPField fld = ... // get the field from the current collection
    ...	

    if (!fld.CanBeDeleted)
        ... // throw an eception

    SPFieldLookup lookup = fld as SPFieldLookup;
    if (((lookup != null) && !lookup.IsDependentLookup) && (lookup.GetDependentLookupInternalNames().ToArray().Length != 0))
        ... // throw an eception     

    ... // delete the field
}

Where the CanBeDeleted property of the field defined as the following:

public bool CanBeDeleted
{
    get
    {
        if (this.AllowDeletion.HasValue)
            return this.AllowDeletion.Value;
        return (!this.FromBaseType && !this.Sealed);
    }
}

*Note: this code is true for both SharePoint 2007 and SharePoint 2010.

As we can see, deleting a list field, SharePoint explicitly analyzes such properties of the field as AllowDeletion, Sealed and FromBaseType. Additionally, experiments show that the ReadOnlyField and Hidden properties are being examined as well (likely it happens somewhere in the unmanaged SharePoint modules).

Workaround

An obvious workaround is, before calling SPField.Delete, change the above properties so that the field would be allowed for deletion. So, taking that into account, I implemented the following method(s) to delete list fields:

public static bool RemoveField(SPField spField)
{
    if (spField == null)
    {
        WriteErrorToLog("spField is null! Please, provide a valid one");
        return false;
    }

    bool res = false;
    try
    {
        // check if it's a ReadOnly field.
        // if so, reset it
        if (spField.ReadOnlyField)
        {
            spField.ReadOnlyField = false;
            spField.Update();
        }

        // check if it's a Hidden field.
        // if so, reset it
        if (spField.Hidden)
        {
            spField.Hidden = false;
            spField.Update();
        }

        // check if the AllowDeletion property is set to false.
        // if so, reset it to true
        if (spField.AllowDeletion == null || !spField.AllowDeletion.Value)
        {
            spField.AllowDeletion = true;
            spField.Update();
        }

        // If the AllowDeletion property is set, 
        // the Sealed property seems not to be examined at all.
        // So the following piece of code is commented.
        /*if(spField.Sealed)
        {
            spField.Sealed = false;
            spField.Update();
        }*/

        // If the AllowDeletion property is set, 
        // the FromBaseType property seems not to be examined at all.
        // So the following piece of code is commented.
        /*if(spField.FromBaseType)
        {
            spField.FromBaseType = false;
            spField.Update();
        }*/

        // finally, remove the field
        spField.Delete();
        spField.ParentList.Update();

        res = true;
    }
    catch (Exception ex)
    {
        WriteErrorToLog(ex.Message);
    }

    return res;
}

public static bool RemoveField(SPList spList, string displayNameOrInternalNameOrStaticName)
{
    SPField spField = GetFieldByName(spList, displayNameOrInternalNameOrStaticName);
    if(spField == null)
    {
        WriteErrorToLog(string.Format("Couldn't find field {0}!", displayNameOrInternalNameOrStaticName));
        return false;
    }

    return RemoveField(spField);
}

public static void WriteErrorToLog(string errorMsg)
{
    // write error into log
}

*Note: the GetFieldByName method is described here – Getting SPField with no exceptions to be thrown.

According to the code of the CanBeDeleted, the Sealed and FromBaseType properties are not being examined at all while the AllowDeletion is set. Thus the dealing with them is commented, but retained just in case.

Below is a sample of use:

SPSecurity.RunWithElevatedPrivileges(delegate
{
    using (SPSite spSite = new SPSite("some site url"))
        using (SPWeb spWeb = spSite.OpenWeb())
        {
            SPList spList = GetListByUrl(spWeb, "Lists/Products");
            RemoveField(spList, "product name");
        }
});

*Note: the GetListByUrl method is described here – Getting SPList with no exceptions to be thrown.

The RemoveField method isn’t a panacea. For example, you still will have problems if the field going to be deleted is a part of a multiple column lookup.

Be very careful when deleting fields, because it can lead to severe issues in your SharePoint applications. If you are uncertain, don’t delete anything, especially if it concerns a live application on a production server. Remember that you are always able to just make the field hidden instead.

SharePoint: Getting a SPField with no exceptions to be thrown

June 29th, 2012 No comments

    As you probably know, a SharePoint field has a few names: display name, internal and static. The display name usually differs from the internal and static ones. In some exotic cases, the internal and static names differ from each other too. List’s fields can be reached through the SPList.Fields collection of the SPFieldCollection class.

Get SPField by different names in SharePoint 2007

In SharePoint 2007 the SPFieldCollection exposes a few methods to get a field by its known display or internal name, but not the static name. These methods are the indexer of the SPFieldCollection that accepts the field’s display name, the GetFieldByInternalName method accepting the internal name, and the GetField method accepting both display and internal names. Unfortunately, all these methods are case sensitive and throw an exception if the field with the passed display or internal name wasn’t found. I don’t like to wrap every piece of code into try-catch, so, for SharePoint 2007 applications I’m involved in, when it’s possible I use the simple method shown below:

public static SPField GetFieldByName(SPList spList, string displayNameOrInternalNameOrStaticName)
{
    displayNameOrInternalNameOrStaticName = displayNameOrInternalNameOrStaticName.ToLower();

    foreach (SPField spField in spList.Fields)
    {
        if (spField.Title.ToLower() == displayNameOrInternalNameOrStaticName)
            return spField;
        if (spField.InternalName.ToLower() == displayNameOrInternalNameOrStaticName)
            return spField;
        if (spField.StaticName.ToLower() == displayNameOrInternalNameOrStaticName)
            return spField;
    }

    return null;
}

// how to use
// ...
    using (SPSite spSite = new SPSite("some site url"))
        using (SPWeb spWeb = spSite.OpenWeb())
        {
            SPList  spList  = GetListByUrl(spWeb, "Lists/Products");
            SPField spField = GetFieldByName(spList, "product name"); // the field's real display name is Product Name
            // do something
        }
// ...

*Note: find the GetListByUrl method in the previous blog post – SharePoint: Getting SPList with no exceptions to be thrown

The GetFieldByName accepts a field’s all possible names including the static name, it’s not case sensitive and returns null if the sought-for field doesn’t exist.

Of course, I’m aware that enumerating fields takes more time than retrieving them from the SPFieldCollection’s internal hashtables so as the built-in methods do. But when time isn’t so crucial for a particular piece of code, I prefer using the GetFieldByName method. In addition I don’t have an alternative for the GetFieldByName when I know only the field’s static name.

Get SPField by different names in SharePoint 2010

In SharePoint 2010 the new TryGetFieldByStaticName method has been added to the SPFieldCollection class. So, as the method’s name implies, we get a field by its static name, and no one exception even will be thrown in case the field doesn’t exist. Thus, for SharePoint 2010 I’ve modified the GetFieldByName as follows:

public static SPField GetFieldByName(SPList spList, string displayNameOrInternalNameOrStaticName)
{
    SPField spFieldByStaticName = spList.Fields.TryGetFieldByStaticName(displayNameOrInternalNameOrStaticName);
    if (spFieldByStaticName != null)
        return spFieldByStaticName;

    displayNameOrInternalNameOrStaticName = displayNameOrInternalNameOrStaticName.ToLower();

    foreach (SPField spField in spList.Fields)
    {
        if (spField.Title.ToLower() == displayNameOrInternalNameOrStaticName)
            return spField;
        if (spField.InternalName.ToLower() == displayNameOrInternalNameOrStaticName)
            return spField;
        if (spField.StaticName.ToLower() == displayNameOrInternalNameOrStaticName)
            return spField;
    }

    return null;
}

Check whether a field exists

To check whether a field exists, we can use the following method based on the GetFieldByName:

public static bool FieldExist(SPList spList, string displayNameOrInternalNameOrStaticName)
{
    return GetFieldByName(spList, displayNameOrInternalNameOrStaticName) != null;
}
Related posts: