Tuesday, July 20, 2010

Create associated Sharepoint groups (AssociatedOwnerGroup, AssociatedMemberGroup and AssociatedVisitorGroup) programmatically

If you create web site in Sharepoint and choose “Use unique permissions” option then after site template will be applied to the newly created site you will be redirected to the standard page “Set Up Groups for this Site” (_layouts/permsetup.aspx page):

image

Using this page you are able to create 3 standard groups for new site: Visitors, Members and Owners. But how these groups can be created programmatically? Suppose that we have customized process of site creation (e.g. use custom site creation page) with custom site template and want to create mentioned groups automatically without additional manual steps required from administrator.

In object model mentioned groups correspond to the following SPWeb properties: AssociatedOwnerGroup, AssociatedMemberGroup and AssociatedVisitorGroup. First of all we need to know what permissions have these groups on associated site. Here they are:

Group Permissions
AssociatedOwnerGroup SPRoleType.Administrator
AssociatedMemberGroup SPRoleType.Contributor
AssociatedVisitorGroup SPRoleType.Reader

Now we can create associated groups programmatically using the following code:

   1: private void setupSecurity(SPWeb web)
   2: {
   3:     // create groups
   4:     string ownersName = SPResource.GetString("DefaultOwnerGroupName",
   5: new object[] {web.Title});
   6:     var owners = SecurityHelper.CreateSiteGroup(web, ownersName);
   7:     owners.Owner = owners;
   8:     owners.Update();
   9:  
  10:     string visitorsName = SPResource.GetString("DefaultVisitorGroupName",
  11: new object[] {web.Title});
  12:     var visitors = SecurityHelper.CreateSiteGroup(web, visitorsName);
  13:     visitors.Owner = owners;
  14:     visitors.Update();
  15:  
  16:     string membersName = SPResource.GetString("DefaultMemberGroupName",
  17: new object[] {web.Title});
  18:     var members = SecurityHelper.CreateSiteGroup(web, membersName);
  19:     members.Owner = owners;
  20:     members.Update();
  21:  
  22:     // assign permissions
  23:     SecurityHelper.AssignGroupRoleToSecurableObject(web, web,
  24: SPRoleType.Reader, visitors);
  25:     SecurityHelper.AssignGroupRoleToSecurableObject(web, web,
  26: SPRoleType.Contributor, members);
  27:     SecurityHelper.AssignGroupRoleToSecurableObject(web, web,
  28: SPRoleType.Administrator, owners);
  29:  
  30:     // associate
  31:     web.AssociatedOwnerGroup = web.SiteGroups[ownersName];
  32:     web.Update();
  33:  
  34:     web.AssociatedMemberGroup = web.SiteGroups[membersName];
  35:     web.Update();
  36:  
  37:     web.AssociatedVisitorGroup = web.SiteGroups[visitorsName];
  38:     web.Update();
  39: }

First of all we need get names of associated groups. In order to get them we use the same approach which is used in OTB permsetup.aspx page, e.g.:

   1: string ownersName = SPResource.GetString("DefaultOwnerGroupName",
   2: new object[] {web.Title});

Also if we want to do things like they done in OTB page, we need to set SPGroup.Owner property to AssociatedOwnerGroup (you can check it after creation of site using OTB create page). AssociatedOwnerGroup will be owner of itself.

After that we need to assign permissions to created site groups using table mentioned above. We do it using the following code:

   1: // assign permissions
   2: SecurityHelper.AssignGroupRoleToSecurableObject(web, web,
   3: SPRoleType.Reader, visitors);
   4: SecurityHelper.AssignGroupRoleToSecurableObject(web, web,
   5: SPRoleType.Contributor, members);
   6: SecurityHelper.AssignGroupRoleToSecurableObject(web, web,
   7: SPRoleType.Administrator, owners);

Here is the code of SecurityHelper utility class:

   1: public static class SecurityHelper
   2: {
   3:     public static SPGroup CreateSiteGroup(SPWeb web, string groupName)
   4:     {
   5:         if (isGroupExist(web, groupName))
   6:         {
   7:             throw new Exception(string.Format("Group '{0}' already exists",
   8:                 groupName));
   9:         }
  10:         web.SiteGroups.Add(groupName, web.SiteAdministrators[0], null,
  11:             string.Empty);
  12:  
  13:         return web.SiteGroups[groupName];
  14:     }
  15:  
  16:     private static bool isGroupExist(SPWeb web, string groupName)
  17:     {
  18:         return web.SiteGroups.Cast<SPGroup>().Any(g =>
  19:             string.Compare(g.Name, groupName, true) == 0);
  20:     }
  21:  
  22:     public static void AssignGroupRoleToSecurableObject(SPWeb web,
  23:         ISecurableObject securableObject, SPRoleType roleType, SPGroup group)
  24:     {
  25:         SPRoleAssignment roleAssignment = new SPRoleAssignment(group);
  26:         SPRoleDefinition roleDefinition = web.RoleDefinitions.GetByType(roleType);
  27:         assignRoleToSecurableObject(securableObject, roleDefinition, roleAssignment, true);
  28:     }
  29:  
  30:     public static void AssignGroupRoleToSecurableObject(SPWeb web,
  31:         ISecurableObject securableObject, SPRoleType roleType, SPGroup group,
  32:         bool copyRoleAssignment)
  33:     {
  34:         SPRoleAssignment roleAssignment = new SPRoleAssignment(group);
  35:         SPRoleDefinition roleDefinition = web.RoleDefinitions.GetByType(roleType);
  36:         assignRoleToSecurableObject(securableObject, roleDefinition, roleAssignment,
  37:             copyRoleAssignment);
  38:     }
  39:  
  40:     private static void assignRoleToSecurableObject(ISecurableObject securableObject,
  41:         SPRoleDefinition roleDefinition, SPRoleAssignment roleAssignment,
  42:         bool copyRoleAssignment)
  43:     {
  44:         roleAssignment.RoleDefinitionBindings.Add(roleDefinition);
  45:         if (!securableObject.HasUniqueRoleAssignments)
  46:         {
  47:             securableObject.BreakRoleInheritance(copyRoleAssignment);
  48:         }
  49:         securableObject.RoleAssignments.Add(roleAssignment);
  50:     }
  51: }

And last step – associate created groups with SPWeb:

   1: // associate
   2: web.AssociatedOwnerGroup = web.SiteGroups[ownersName];
   3: web.Update();
   4:  
   5: web.AssociatedMemberGroup = web.SiteGroups[membersName];
   6: web.Update();
   7:  
   8: web.AssociatedVisitorGroup = web.SiteGroups[visitorsName];
   9: web.Update();

Remaining question is where should we call this setupSecurity(…) method in our customized site creation process? One obvious place – feature receiver in some feature which is activated in our site automatically (this feature should be added into onet.xml of our custom site template). Unfortunately if you create custom portal based on OTB publishing site template, it is not suitable place – because Sharepoint will override associated groups somewhere in later execution steps (you will have null in SPWeb properties which correspond to associated groups, except Visitors group). One suitable place I found for this – is custom portal provisioning provider:

   1: public class CustomPortalProvisioningProvider : SPWebProvisioningProvider
   2: {
   3:     public override void Provision(SPWebProvisioningProperties props)
   4:     {
   5:         // PortalProvisioningProvider is sealed so aggregate it instead of inheritance
   6:         var publishingPortalProvisioningProvider =
   7:             new PortalProvisioningProvider();
   8:         publishingPortalProvisioningProvider.Provision(props);
   9:  
  10:         this.setupSecurity(props.Web);
  11:     }
  12:     
  13:     ...
  14: }

But this approach has own problem: code will be executed using language of site in context of which site creation process was started, i.e. not in context of newly created site. It means that if for example you opened custom site creation page under English site and create Swedish subsite, associated groups names will still have English titles (Owners, Members, Visitors). In order to make group titles on the language of created site we need temporary change locale of current thread. In one of my previous posts I already showed how to make it:

   1: var currentCulture = Thread.CurrentThread.CurrentCulture;
   2: var currentUICulture = Thread.CurrentThread.CurrentUICulture;
   3: try
   4: {
   5:     SPUtility.SetThreadCulture(web);
   6:  
   7:     setupSecurity(web);
   8: }
   9: finally
  10: {
  11:     Thread.CurrentThread.CurrentCulture = currentCulture;
  12:     Thread.CurrentThread.CurrentUICulture = currentUICulture;
  13: }

Now if you will create Swedish site, associated groups will have Swedish titles: Ägare av test, Medlemmar på test, Besökare på test (test is web site title).

Tuesday, July 13, 2010

Internal mechanism of showing indicator of long operations in Sharepoint using SPLongOperation

In this post I’m going to show how implemented indicator of long operations in Sharepoint internally. Most of posts about SPLongOperation are limited by examples of using it. I will go further and show how it is implemented internally. So lets start.

If you worked with Sharepoint then you will probably saw OTB indicator of long operations:

image

This indicator also can be used in your custom code, e.g. in handler of button OnClick event or any other place. In order to show it you need to use SPLongOperation class (defined in Microsoft.SharePoint.dll). The usage is quite simple: all you need is to create instance of SPLongOperation, call Begin method at the beginning of operation and End method on the end. Also you can specify your own title and description for long operation.

For example I created simple application _layouts page:

   1: <%@ Page Language="C#" %>
   2: <%@ Import Namespace="Microsoft.SharePoint" %>
   3: <%@ Import Namespace="System.Threading" %>
   4:  
   5: <html xmlns="http://www.w3.org/1999/xhtml" >
   6: <head>
   7:     <title>Long operation example</title>
   8: </head>
   9: <body>
  10:     <form id="form1" runat="server">
  11:     <script runat="server">
   1:  
   2:         protected void btn_OnClick(object sender, EventArgs e)
   3:         {
   4:             using (var operation = new SPLongOperation(this))
   5:             {
   6:                 operation.LeadingHTML = "My long operation";
   7:                 operation.TrailingHTML = "Description of long operation";
   8:             
   9:                 operation.Begin();
  10:                 
  11:                 Thread.Sleep(5000); // simulate long operation
  12:                 
  13:                 operation.End(this.Request.RawUrl);
  14:             }
  15:         }        
  16:     
</script>
  12:  
  13:     <asp:Button ID="btn" runat="server" Text="Click" OnClick="btn_OnClick" />
  14:  
  15:     </div>
  16:   </form>
  17: </body>
  18: </html>
  19:  

There is a button on the page. When user clicks this button the following indicator of long operation will be shown:

image

After 5 seconds indicator is hided and page is shown again. Note that operation will be executed synchronously so if it will take more time than request timeout specified in your web application you will get Request timeout exception. And you will understand why it happens when see the rest of this article.

Most of posts which tell about SPLongOperation are finished in this place. Lets go further and see under the hood. What happens when you call SPLongOperation.Begin() method? Lets see this method using reflector:

   1: public void Begin()
   2: {
   3:     string s = GearFileContent.Replace(
   4: "<%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%>",
   5: Thread.CurrentThread.CurrentUICulture.LCID.ToString(CultureInfo.InvariantCulture));
   6:     this.m_srGearAspx = new StringReader(s);
   7:     this.WriteGearToSearchString(m_strBeginContent);
   8:     this.m_Page.Response.Write("<div id=GearPage>");
   9:     this.WriteGearToSearchString(m_strEndContent);
  10:     this.m_Page.Response.Write("</div>");
  11:     this.WriteGearToSearchString(m_strTargetDots);
  12: }

It reads content of gear.aspx page which is located in 12/template/layouts folder on file system. Then it reads content line by line and replaces placeholders by real values. At first it replaces System.Threading.Thread.CurrentThread.CurrentUICulture.LCID placeholder by real integer value of current locale. So css path from

   1: <link rel="stylesheet" type="text/css"
   2: href="/_layouts/<%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%>/styles/core.css" />

will be:

   1: <link rel="stylesheet" type="text/css" href="/_layouts/1033/styles/core.css" />

if you use English locale (lcid = 1033). Then it replaces resources placeholders, so strings like:

   1: <HTML dir="&lt;SharePoint:EncodedLiteral runat='server'
   2: text='<%$Resources:wss,multipages_direction_dir_value%>' EncodeMethod='HtmlEncode'/>">

will be expanded to:

   1: <HTML dir="ltr">

I.e. code retrieves multipages_direction_dir_value resource object from wss.resx file and replaces it in output html.

If you will see inside gear.aspx file you will find several other placeholders in layout:

   1: <html>
   2:         <head>
   3:                 <title>
   4:                     ...
   5:                 </title>
   6:                 ...
   7:                 <script language="javascript">
   1:  
   2:                    function gotoNextPage() { }
   3:                 
</script>
   8:         </head>
   9:         <body onload="javascript:gotoNextPage()">
  10:         SPLongOperation.BeginContent
  11:             ...
  12:             <!-- LEADING HTML -->
  13:             </span><span class='ms-descriptiontext'>
  14:             <!-- TRAILING HTML -->
  15:             ...
  16:         SPLongOperation.EndContent
  17:         SPLongOperation.Dots
  18:         <script language="javascript">
  19:         function gotoNextPage()
  20:         {
  21:             window.location.replace("SPLongOperation.RedirectUrl");
  22:         }
  23:         </body>
  24: </html>

Placeholders SPLongOperation.BeginContent, SPLongOperation.EndContent, SPLongOperation.Dots are replaced by empty string. Placeholders <!-- LEADING HTML –> and <!-- TRAILING HTML –> are replaced by LeadingHTML and TrailingHTML properties of SPLongOperation object.

Now very important moment: in the Begin() method SPLongOperation writes response until SPLongOperation.Dots placeholder (see code above). So html will NOT contain the following lines:

   1: <script language="javascript">
   2: function gotoNextPage()
   3: {
   4:     window.location.replace("SPLongOperation.RedirectUrl");
   5: }
   6: </body>

You can check it by yourself if you will click View Source when long operation indicator will be shown. See that at the top of gear.aspx page there is another javascript function with the same name gotoNextPage but with empty body:

   1: <html>
   2:         <head>
   3:                 <title>
   4:                     ...
   5:                 </title>
   6:                 ...
   7:                 <script language="javascript">
   1:  
   2:                    function gotoNextPage() { }
   3:                 
</script>
   8:         </head>
   9:         <body onload="javascript:gotoNextPage()">
  10:         ...

And this function is assigned to onload event of body element. It means that until another function with non-empty body will be written to the response, first loaded function with empty body will be used (actually this 1st function is not called at all because body element is not fully loaded). And as you probably already guess SPLongOperation.End(…) method replaces SPLongOperation.RedirectUrl placeholder by redirect URL (in example above this is the same page as was initially requested) and writes the rest of file to the response:

   1: window.location.replace("SPLongOperation.RedirectUrl");

is replaced by

   1: window.location.replace("/_layouts/test.aspx");

(I assumed that URL of testing page is test.aspx). Unfortunately SPLongOperation.End(…) method is obfuscated but I found one single post here which shows implementation of this method:

   1: public void End(string strProposedRedirect, SPRedirectFlags rgfRedirect,
   2: HttpContext context, string queryString)
   3: {
   4:     string str;
   5:     if (!this.m_bLongOperationEnded)
   6:     {
   7:         this.m_bLongOperationEnded = true;
   8:         lock (this.lockObject)
   9:         {
  10:             this.m_bLongOperationStarted = false;
  11:             if (this.m_Timer != null)
  12:             {
  13:                 this.m_Timer.Dispose();
  14:                 this.m_Timer = null;
  15:             }
  16:             if (this.m_ichLine > -1)
  17:             {
  18:                 this.WriteGearRestOfLine(m_strTargetDots);
  19:             }
  20:             if (!SPUtility.DetermineRedirectUrl(strProposedRedirect,
  21: rgfRedirect, context, queryString, out str))
  22:             {
  23:                 str = strProposedRedirect;
  24:             }
  25:         }
  26:     }
  27:     else
  28:     {
  29:         return;
  30:     }
  31:     this.WriteGearToSearchString(m_strTargetRedir);
  32:     if (this.m_ichLine > -1)
  33:     {
  34:         string s = SPHttpUtility.EcmaScriptStringLiteralEncode(str);
  35:         this.m_Page.Response.Write(s);
  36:         this.WriteGearRestOfLine(m_strTargetRedir);
  37:     }
  38:     this.WriteGearRemaining();
  39:     this.m_srGearAspx.Close();
  40:     this.m_srGearAspx = null;
  41:     this.m_Page.Response.Flush();
  42:     this.m_Page.Response.End();
  43: }

So it uses known feature of javascript which allows to override functions by defining function with the same name in later execution step (e.g. in the end of page). After that SPLongOperation.End(…) method calls Response.End() which in turn caused ThreadAbortException. So execution after SPLongOperation.End(…) is interrupted and response is given to client. After this body onload handler is raised and overridden gotoNextPage function is called which redirects you on the specified URL.

That how SPLongOperation is implemented internally. Hope it will help you to understand Sharepoint mechanisms more deeply.

Saturday, July 3, 2010

Speaking on LINQ-building in Sharepoint seminar in Microsoft

Yesterday I and Vladimir Timashkov were speaking on developers seminar in MS office in Saint-Petersburg. Vladimir made an overview of data retrieving in Sharepoint and summarized their advantages and disadvantages. I told about Camlex.NET open source project – show its internal architecture, examples of practical usages and how we worked over it. Also we told about our experience of participation in open source projects. In another sessions Vitaly Baum told about Linq 2 Sharepoint 2010 and OData, and Michael Arhipov told about custom implementation of Linq 2 Sharepoint provider. I think such meetings are quite useful for developers as here we can share our knowledge with each other and increase our professional skills.

I uploaded our presentation on the slideshare so it can be viewed online: http://www.slideshare.net/sadomovalex/slides-4670911. Here is also link on Vitaly’s presentation: http://www.slideshare.net/butaji/sharepoint-openxml.