Sign In
The Sug > Blogs > // todo: be awesome
March 05
Custom Retention Policy Formula

SharePoint 2010's document retention feature is really useful, but what if you need to specify the expiration date based on a custom field instead of a hard coded value.  This is possible and surprisingly straight forward to do.  You just need to create a class that implements Microsoft.Office.RecordsManagement.PolicyFeatures.IExpirationFormula and register it as a policy resource.

The first thing you will need to do is add a reference to the Microsoft.Office.Policy.dll library to your project in Visual Studio.  Then you need to create a class and implement IExpirationFormula.  This interface declares one method:

public DateTime? ComputeExpireDate(SPListItem item, XmlNode parametersData)

Note that it takes an SPListItem as a parameter.  The item passed in here is the item you are calculating the expiration date for, so you will have full access to the field values for the item.  For example, if you want to calculate the expiration date for the item as being X days from the day it was created, and X is stored as an integer in a field called RetentionDays, your method might look something like this:

public DateTime? ComputeExpireDate(SPListItem item, XmlNode parametersData)
{
    object created = item["Created"];
    object retention = item["RetentionDays"];
 
    DateTime? expires = null;
    DateTime createdDate = (DateTime)created;
    int retentionDays = 0;
 
    if (retention != null)
        int.TryParse(retention.ToString(), out retentionDays);
 
    if (retentionDays > 0)
        expires = createdDate.AddDays(retentionDays);
 
    return expires;
}

You will notice that if the RetentionDays field is not set to a value greater than 0, the expiration date is never calculated and null is returned.  This effectively tells SharePoint that there is no expiration date for this item.

Now that you have a custom formula in a class, you just need to register it as a policy resource.  This is easily accomplished with a bit of XML and a Farm scoped feature.

So add a new feature to your project, set the scope to Farm and add an event receiver.  I haven't found an out-of-the-box event receiver that handles this, and I haven't taken the time to create one yet, so you will need to include the XML directly in the receiver.  Override the FeatureActivated method and add the following code:


string xmlManifest = "<PolicyResource xmlns=\"urn:schemas-microsoft-com:office:server:policy\"" +
    " id = \"Unique ID for this resource\"" +
    " featureId=\"Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration\"" +
    " type = \"DateCalculator\">" +   
    "<Name>Retention Policy Test</Name>" +
    "<Description>Items Expire based on the specified retention schedule</Description>" +
    "<AssemblyName>Fully qualified assembly name that holds the custom formula class</AssemblyName>" +
    "<ClassName>Fully qualified custom formula class name</ClassName>" +
    "</PolicyResource>";
 
PolicyResource.ValidateManifest(xmlManifest);
PolicyResourceCollection.Add(xmlManifest);
The PolicyResource and PolicyResourceCollection classes are available in the Microsoft.Office.RecordsManagement.InformationPolicy namespace.

Take note of the value you enter in the id attribute.  It must be unique and if you plan to add code to remove the policy resource later (we will do so in the FeatureDeactivating event), you will need it.  The AssemblyName element and the ClassName element should hold the fully qualified names of the assembly and class that hold the custom formula.

The PolicyResource.ValidateManifest method will throw an exception if the XML is not valid, so handle that more gracefully if you wish.

Now, you will also want to provide a way to remove the formula as a resource, so override the FeatureDeactivting method and include this single line of code:

PolicyResourceCollection.Delete("Unique ID for this resource");

Ensure you are using the same ID value here as you did in the FeatureActivated method.

At this point, you have everything you need, so simply deploy the solution and activate the feature.  You will then be able to use your custom formula from the UI.
January 27
Creating a site definition with sub sites
Site definitions are fairly easy to create for SharePoint 2010 when using Visual Studio 2010.  However, they are limited to a single site.  If you need to create a hierarchy of sites, you will need to have a custom site provisioning provider and add sites through code.  Thankfully, Microsoft has provided this for us in the PortalProvisioningProvider.

The steps:
  1. Create a site definition
  2. Create an XML file that describes the hierarchy
  3. Attach the PortalProvisioningProvider to the definition
  4. Attach the XML file as the provisioning data

The XML file needs to be in a specific format as follows:


<?xml version="1.0" encoding="utf-8" ?>
<portal xmlns="PortalTemplate.xsd">
  <web name="Home" siteDefinition="CustomSite#0" displayName="Home" description="The home site">
    <webs>
      <web name="SubSite1" siteDefinition="CustomSubSite#0" displayName="Sub1" description="The first sub site" />
      <web name="SubSite2" siteDefinition="CustomSubSite#0" displayName="Sub2" description="The second sub site">
        <webs>
          <web name="SubSite2SubSite" siteDefinition="CustomSubSite#0" displayName="Sub2 Sub" description="A sub sub site" />
        </webs>
      </web>
    </webs>
  </web>
</portal>


As you can see, you can create any number of sites with any number of sub sites, and the siteDefinition attribute is in the "Name#Configuration ID" format. 

You attach this XML as well as the provisioning provider to the site template through the webtemp file.  The CustomSite webtemp might look something like this:


<?xml version="1.0" encoding="utf-8"?>
<Templates xmlns:ows="Microsoft SharePoint">
  <Template Name="CustomSite" ID="100100">
    <Configuration 
      ID="0" 
      Title="Custom Site" 
      Hidden="FALSE" 
      ImageUrl="/_layouts/images/CPVW.gif" 
      Description="A custom site" 
      DisplayCategory="Custom Sites"
      ProvisionAssembly="Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
      ProvisionClass="Microsoft.SharePoint.Publishing.PortalProvisioningProvider"
      ProvisionData="SiteTemplates\\CustomSite\\Xml\\portal.xml" />
  </Template>
</Templates>


Notice how the XML file is referenced?  It needs to be added as an element file so that it gets deployed to the hive.  I find it best to put it somewhere under the custom site template directory.

As a special note, if you are creating a hierarchy of sub sites that are all using custom site templates.  You may want to make the sub site templates hidden unless you need to be able to create them outside of the hierarchy.
January 06
Setting the default page layout for a site.
SharePoint 2010 has this new feature where you can create a page directly in a publishing site without further intervention.



Microsoft was also kind enough to provide a way to set the default page layout the new page will be created with.  Just go to Site Settings and click Page layouts and site templates under the Look and Feel section.  You will be presented with this:



This is great, but what if you need to set the default layout via code?  Suppose you are creating a site definition in Visual Studio that contains a feature that is activated when the site is created and you want to set the default page layout to a specific layout.  This can be done.  The approach I would take is to create a web scoped feature that is activated when the site is created.  This feature would have a feature receiver that sets the default page layout when it is activated.  The code looks something like the following:



SPWeb web = properties.Feature.Parent as SPWeb;
if (web == null || !PublishingWeb.IsPublishingWeb(web))
    return;
 
PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
PageLayout[] layouts = pubWeb.GetAvailablePageLayouts();
 
if (layouts.Length > 0)
{
    PageLayout layout = layouts.FirstOrDefault(pl => pl.Name == "LayoutName.aspx");
    if (layout != null)
    {
        pubWeb.SetDefaultPageLayout(layout, true);
        pubWeb.Update();
    }
}


And that's it!
December 08
Contextual Search Doesn't Return Results?
If you have ever found that contextual search (this site, site list) isn't returning any results, but you can see results from the context in the All Sites scope, you need to check the AAM for the farm. Contextual search will not work if the default zone for the web application doesn't have the same url as the site being searched. This commonly comes into play when a site was setup as http initially and added https later. The default zone will be set to http, but the site you access may be https. In this situation, you simply need to make https your default zone.
November 22
SPContext & HttpModules
HttpModules are a great way to process data for specific types of requests coming into your web site.  Implementing them is easy.  You simply create a class that implements IHttpModule, handle the appropriate event, and register it in your web.config file.

For reference, the events (in order) are as follows:
  1. BeginRequest
  2. AuthenticateRequest
  3. AuthorizeRequest
  4. ResolveRequestCache
  5. AcquireRequestState
  6. PreRequestHandlerExecute
  7. PostRequestHandlerExecute
  8. ReleaseRequestState
  9. UpdateRequestCache
  10. EndRequest

The earliest event you can handle and get a valid reference to the current SPContext is PreRequestHandlerExecute.  Keep this in mind when your module needs to work with SharePoint data.

November 04
WebPart Life Cycle
Because I consistently forget and have to look it up, I decided to post the life cycle of an ASP.NET WebPart.

On page load:
  1. OnInit
  2. OnLoad
  3. Connection Consuming
  4. CreateChildControls
  5. OnPreRender
  6. Render

On post back:

  1. OnInit
  2. CreateChildControls
  3. OnLoad
  4. Page Control Event Handling (button clicks, drop down index changes, etc.)
  5. Connection Consuming
  6. OnPreRender
  7. Render

Note how the connection code runs before CreateChildControls on load and after it on post back.  If you write code where CreateChildControls depends on a value that is set from a connection, you will need to do something creative to handle it. 

You could, for example, clears the controls, and recreate them:



this.Controls.Clear();
this.ChildControlsCreated = false;
this.EnsureChildControls()


October 28
Multi-Group Validation Summary
This isn't strictly SharePoint related, but it's a useful enough solution to a fairly common problem that I thought it worth my time to write about.  The ASP.Net ValidationSummary control only works with a single ValidationGroup.  But what if your validators are spread across multiple validation groups for whatever reason?  You can add multiple validation summary controls, but you then have to do some extra work to manipulate the output so everything appears as one summary.  A better solution is to create a custom ValidationSummary that can handle multiple validation groups.  And so was born the MultiGroupValidationSummary.

The first thing I did when investigating this was fired up ILSpy (similar to Reflector) and took a look at the code for the ValidationSummary to see why it would not work with multiple groups.  I found that the Render method ends up calling an internal method called GetErrorMessages that returns the validators on the page using the following:



ValidatorCollection validators = this.Page.GetValidators(this.ValidationGroup);



Well that explains it!  Since GetErrorMessages is not virtual, we can't just create a child class and override this method.  But we can override the Render method and have it call a custom version of GetErrorMessages. 

My custom class also adds two new properties:
  1. Mode - an enumeration value to determine if the control will use a single validation group, multiple validation groups, or all validation groups
  2. ValidationGroups - a comma separated string value that specifies the validation groups to use when the mode is set to multiple validation groups.

My enumeration looks like this:


public enum ValidationMode
{
    SingleGroup = 0,
    MultiGroup = 1,
    AllGroups = 2
}


The class itself inherits from ValidationSummary (of course)


public class MultiGroupValidationSummary : ValidationSummary
{
    // would probably be good to store these properties in ViewState
    public ValidationMode Mode { get; set; }  
    public string ValidationGroups { get; set; }

    public MultiGroupValidationSummary()
        : base()
    {
        // default to use all validation groups
        this.Mode = ValidationMode.AllGroups;
    }

    // Render override...

    // Custom GetErrorsMessages method
}


The Render override is essentially just a copy of what ILSpy shows for the ValidationSummary.  I cleaned it up a bit and removed anything I couldn't access from the base class. I believe everything I removed is going to be non-essential for the majority of cases.  However, be aware that since some things were removed, it's possible the control won't function as you expect in some rare cases.


protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
    if (!this.Enabled)
        return;

    bool hasErrors;
    string[] errors = this.GetErrorMessages(out hasErrors);
    hasErrors = this.ShowSummary && hasErrors;

    if (this.Page != null)
        this.Page.VerifyRenderingInServerForm(this);

    if (hasErrors)
    {
        this.RenderBeginTag(writer);

        string textAfterHeader = null;
        string textBeforeErrors = null;
        string textBeforeError = null;
        string textAfterError = null;
        string textAfterErrors = null;

        switch (this.DisplayMode)
        {
            case ValidationSummaryDisplayMode.List:
                textAfterHeader = "<br />";
                textBeforeErrors = string.Empty;
                textBeforeError = string.Empty;
                textAfterError = "<br />";
                textAfterErrors = string.Empty;
                break;
            case ValidationSummaryDisplayMode.SingleParagraph:
                textAfterHeader = " ";
                textBeforeErrors = string.Empty;
                textBeforeError = string.Empty;
                textAfterError = " ";
                textAfterErrors = "<br />";
                break;
            default:
                textAfterHeader = string.Empty;
                textBeforeErrors = "<ul>";
                textBeforeError = "<li>";
                textAfterError = "</li>";
                textAfterErrors = "</ul>";
                break;
        }
 
        if (this.HeaderText.Length > 0)
        {
            writer.Write(this.HeaderText);
            writer.Write(textAfterHeader);
        }

        if (errors != null)
        {
            writer.Write(textBeforeErrors);
            for (int i = 0; i < errors.Length; i++)
            {
                writer.Write(textBeforeError);
                writer.Write(errors[i]);
                writer.Write(textAfterError);
            }
            writer.Write(textAfterErrors);
        }

        this.RenderEndTag(writer);
    }
}


And finally, the customized GetErrorMessages method that takes the Mode into account looks something like this:



private string[] GetErrorMessages(out bool hasErrors)
{
    hasErrors = false;

    ValidatorCollection validators = null;
    switch (this.Mode)
    {
        case ValidationMode.SingleGroup:
            validators = this.Page.GetValidators(this.ValidationGroup);
            break;
        case ValidationMode.MultiGroup:
            validators = new ValidatorCollection();
            if (!string.IsNullOrEmpty(this.ValidationGroups))
            {
                string[] groups = this.ValidationGroups.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string group in groups)
                {
                    foreach (IValidator validator in this.Page.GetValidators(group.Trim()))
                    validators.Add(validator);
                }
            }
            break;
        case ValidationMode.AllGroups:
            validators = this.Page.Validators;
            break;
    }

    int num = 0;
    for (int i = 0; i < validators.Count; i++)
    {
        IValidator validator = validators[i];
        if (!validator.IsValid)
        {
            hasErrors = true;
            if (validator.ErrorMessage.Length != 0)
                num++;
        }
    }

    string[] errors = null;
    if (num != 0)
    {
        errors = new string[num];
        int num2 = 0;
        for (int j = 0; j < validators.Count; j++)
        {
            IValidator validator2 = validators[j];
            if (!validator2.IsValid && validator2.ErrorMessage != null && validator2.ErrorMessage.Length != 0)
            {
                errors[num2] = string.Copy(validator2.ErrorMessage);
                num2++;
            }
        }
    }

    return errors;
}


And your done!  You can now reference this in an aspx page like any custom control and it will show error messages from any number of validation groups.

October 14
Secured "Summary Links" Web Part

Have you ever needed to display links on a page for certain groups of people?  Have you ever thought, "Hey, why can't I secure the links in this summary links web part?"  You are not alone.

While you could accomplish this with some custom code, it can be done OOTB with a links list and a content query web part (CQWP). 

The list is straight forward. Just create a list based on the Links template and secure the list itself or each individual item as required.  The content query web part will need a little massaging to display the links properly.

First, you will need to export a baseline CQWP and add the URL field to the common view fields so you can access it via the XSLT.  You will also need to set the UseCopyUtil property to false.  This will enable the XSLT templates that build item url's to use a custom field rather than a default value.


<property name="CommonViewFields" type="string">URL,URL</property>
<property name="UseCopyUtil" type="bool">False</property>



Import that back into the web part gallery (or don't... you can always just import it onto a page) and then add it to a page.  Setup the web part so it only gets Links list items (set the list type to Links and the content type to Link). 

Next you need to add a new item style to handle the links. You will need to modify the ContentQueryMain.xsl file and the ItemStyle.xsl file (both in the XSL Style Sheets folder of the Style Library). 

You need to add a template to the ContentQueryMain.xsl file that gets the title from a URL field value.  A URL field value stores the url and the title as a comma separated string, so something like this will work:



<xsl:template name="OuterTemplate.GetUrlFieldValueTitle">
  <xsl:param name="Value" />
  <xsl:if test="not(contains($Value,', '))">
            <xsl:value-of select="$Value"/>
  </xsl:if>
  <xsl:if test="contains($Value,', ')">
      <xsl:call-template name="OuterTemplate.Replace">
                <xsl:with-param name="Value" select="substring-after($Value,', ')"/>
                <xsl:with-param name="Search" select="',,'"/>
                <xsl:with-param name="Replace" select="','"/>
      </xsl:call-template>
  </xsl:if>
</xsl:template>



You then need to add the new item style template to the ItemStyle.xsl file.  The example below just displays each link on a line with the notes below it.  Obviously, this can be changed to any style you need.  The important thing is to set the DisplayTitle variable to the value from the new template above and the SafeLinkUrl to use the URL column.



<xsl:template name="Links" match="*" mode="itemstyle">
        <xsl:variable name="SafeLinkUrl">
            <xsl:call-template name="OuterTemplate.GetSafeLink">
                <xsl:with-param name="UrlColumnName" select="'URL'"/>
            </xsl:call-template>
        </xsl:variable>
        <xsl:variable name="DisplayTitle">
            <xsl:call-template name="OuterTemplate.GetUrlFieldValueTitle">
                <xsl:with-param name="Value" select="@URL"/>
            </xsl:call-template>
        </xsl:variable>
        <div id="linkitem" class="item">
            <div class="link-item">
                <a href="{$SafeLinkUrl}" title="
{@LinkToolTip}">
                    <xsl:value-of select="$DisplayTitle"/>
                </a>
                <div class="description">
                    <xsl:value-of select="@Comments" />
                </div>
            </div>
        </div>
</xsl:template>
 



Finally, just set the item style of the CQWP to the new Links style and save the web part.

You now have a secured links web part!
September 30
Creating SPContext when you are out of context

Sometimes you need to write code that runs out of context, but you need it to appear to run in the context of a site and/or web.  I have found this to be true when trying to use certain classes in the API, though I can't say for certain which ones at the moment.  I know there are a few classes in Microsoft.SharePoint.Publishing that assume you are running in context.  In any case, since the SharePoint context site and web are stored in the HttpContext, creating it on the fly is actually fairly simple.  An example method is shown below


using System.Web;
using System.IO;

public void CreateSPContext(string url, SPSite site, SPWeb web)
{
    // create the HttpContext instance
    HttpRequest request = new HttpRequest(string.Empty, url, string.Empty);
    HttpResponse response = new HttpResponse(new StringWriter());
    HttpContext.Current = new HttpContext(request, response);

    // add the site and web to the context
    HttpContext.Current.Items["HttpHandlerSPSite"] = site;
    HttpContext.Current.Items["HttpHandlerSPWeb"] = web;
}



Just call this at the beginning of your out-of-context application (console app, stsadm command, etc.) and you will have a context to work with.  Be sure to dispose of the HttpContext instance before you exit your application.

Note that I haven't tested this on a SharePoint 2010 site yet, but it does work for SharePoint 2007.
September 16
SharePoint 2007 to 2010 account migration
So I ran into a situation recently where a client had upgraded their SharePoint 2007 site to a claims based 2010 site.  I know, it's not all that uncommon. However, they had a custom claims provider in place that replaced the OOTB FBA claims provider.  The side effect:  all of the permissions site wide were broken!

When you upgrade a site, the accounts are not automatically upgraded, so an FBA user with the login name AwesomeMembershipProvider:Jared will remain as such after the upgrade.  The same goes for roles... AwesomeRoleProvider:Role remains AwesomeRoleProvider:Role.

The OOTB FBA claims provider will handle issuing claims that relate to these accounts, and so your site security will remain in tact after the upgrade.  But if you replace this claims provider, your user's will not be issued the proper claims, and they will lose access to parts of the site that they may have had before the upgrade.

The solution?  Migrate the SharePoint accounts to 2010 claims accounts.  You could try to do this with the SPWebApplication.MigrateUsers(true).  The documentation for this method does state that it migrates all accounts to claims accounts, right? However, it isn't smart enough to recognize the difference between users and roles.  So while AwesomeMembershipProvider:Jared is correctly migrated to i:0#.f|AwesomeMembershipProvider|Jared, AwesomeRoleProvider:Role is incorrectly migrated to i:0#.f|AwesomeRoleProvider|Role.  Did you spot the problem?  The role account was migrated as a user claim account!  It should have a prefix of c:0-.f rather than i:0#f (note that i and c are the default claims FBA providers provided by Microsoft for membership and roles respectively).

The way around this is to migrate each account separately.  I wrote a fairly simple PowerShell script to handle this:
$siteUrl = "http://www.yoursite.com"
$mpName = "awesomemembershipprovider";
$rpName = "awesomeroleprovider";
$spFarm = [Microsoft.SharePoint.Administration.SPfarm]::Local;
$site = New-Object Microsoft.SharePoint.SPSite("$siteUrl");
$mpSearch = "$mpName" + ":";
$rpSearch = "$rpName" + ":";

$site.RootWeb.SiteUsers | ForEach-Object {
$name = $_.LoginName.ToLower();
if ($name.StartsWith("$mpSearch"))
$newName = $name.Replace("$mpSearch", “i:0#.f|$mpName|”);
"Migrating User: $name to $newName";
$spFarm.MigrateUserAccount($name, $newName, $False);
else 
{
if ($name.StartsWith("$rpSearch")) 
{
$newName = $name.Replace("$rpSearch", “c:0-.f|$rpName|”);
"Migrating Role: $name to $newName";
$spFarm.MigrateUserAccount($name, $newName, $False);
}
}
}
$site.Dispose();

After you run this, all your accounts will be migrated with the correct login name and your site security should work.

Note, that if you have already added an account with the claims login name to your site, it will be removed when you run this.  For example, if you used the people picker and added a role to a SharePoint Group, then that account will be deleted when that old role account is migrated.

So you should probably run the script right after you upgrade the site.
 

 About this blog

 
About this blog
Welcome to SharePoint Blogs. Use this space to provide a brief message about this blog or blog authors. To edit this content, select "Edit Page" from the "Site Actions" menu.