Joel's SharePoint Architect Blog

SharePoint 2010, MOSS & WSS Tips and Consultancy Tales

Subscribe Subscribe  View Joel Jeffery's profile on LinkedIn
joelblogs.co.uk | joelj.co.uk | joeljeffery.co.uk | jfdiphoenix.co.uk

Posts Tagged ‘SharePoint 2010’

SharePoint 2007 had a commonly used feature that enabled users to create views on lists that grouped by Content Type.

For some reason, this feature was removed from the user interface in SharePoint 2010.

Solution 1: The Easy Method

If you wish to do this today, you can do this using SharePoint Designer to create a view and then change the Xsl to specify a different field name to group by (e.g. “ContentType”).

Overriding the Field Used for Grouping

Solution 2: The Better Method

Alternative, we could try and get our options added to the ViewEdit.aspx page. Options aren’t great for this as it’s a _layouts (application) page, and therefore we can’t just edit it in the browser or SharePoint Designer.

You could add a piece of JavaScript to do this though. Plan a) would be to add this to the bottom of you v4.master, and customise this for the whole site/site collection.

Plan b) would be to create something like a sandbox solution that deploys a “scriptlink” element, placing the script on every page that gets rendered.

I’ve create a CodePlex project for plan b). Here’s some of the code. Firstly, here’s the JavaScript I’d like to run on every page. It simply creates a new <option> tag in HTML and adds it to the drop down list if it exists on the page. Let’s call it “ListViewEdit.js”.

_spBodyOnLoadFunctionNames.push("jbCTFix");

function jbCTFix() {
    jbCTKludge('idGroupField1');
    jbCTKludge('idGroupField2');
}
function jbCTKludge(selName) {
    var sel = document.getElementById(selName);
    if (sel) {
        if (sel.selectedIndex >= 0) {

            var o = document.createElement('option');
            o.text = 'Content Type';
            o.value = 'tp_ContentType';

            var prev = sel.options[sel.selectedIndex];
            try {
                sel.add(o, prev);
            }
            catch (ex) {
                sel.add(o, sel.selectedIndex);
            }
        }
    }
}

Next, here’s the element manifest to apply this on each page in the site collection.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction Id="Ribbon.Library.Actions.Scripts"
              Location ="ScriptLink"
              ScriptSrc="~site/ListViewEdit/ListViewEdit.js" />
  <Module Name="ListViewEdit">
    <File Path="ListViewEdit\ListViewEdit.js" Url="ListViewEdit/ListViewEdit.js" />
  </Module>
</Elements>

You can download the full project and source code for the SharePoint 2010 ViewEdit Group by Content Type project from the CodePlex project here: sp10ctgrouping.codeplex.com

Technorati Tags: Content Types, SharePoint, SharePoint 2010, SharePoint Development

One of my students tonight asked if it was possible to add a condition to a SharePoint Designer 2010 declarative workflow to detect if the initiating user is a member of a particular audience.

There’s nothing built-in to deliver this in SharePoint 2010.

So I knocked-up the following solution based upon the excellent reference implementations of workflows from the SharePoint Prescriptive Guidance Pack at spg.codeplex.com.

I’ve put the full version of my source code and a completed release up on CodePlex.

Firstly, the .Actions file, which must be deployed to 14\\Template\\Xml\\1033\\Workflow:

<?xml version="1.0" encoding="utf-8" ?>
<WorkflowInfo>
  <Conditions And="and" Or="or" Not="not" When="If" Else="Else if">
    <Condition Name="User is member of audience"
        FunctionName="IsUserMemberOfAudienceCondition"
        ClassName="joelblogs.co.uk.WorkflowActivities.AudienceActivity.AudienceMemberActivity"
        Assembly="joelblogs.co.uk.WorkflowActivities.AudienceActivity,
          Version=1.0.0.0, Culture=neutral, PublicKeyToken=d926d259b11539d4"
        AppliesTo="all"
            UsesCurrentItem="True">
      <RuleDesigner Sentence="The user is a member of audience %1">
        <FieldBind Id="1" Field="_1_" Text=""/>
      </RuleDesigner>
      <Parameters>
        <Parameter Name="_1_" Type="System.String, mscorlib" Direction="In" />
      </Parameters>
    </Condition>
  </Conditions>
</WorkflowInfo>

Next, the workflow activity class itself:

using System;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WorkflowActions;
using Microsoft.Office.Server.Audience;
using System.Workflow.ComponentModel;
using System.ComponentModel;

namespace joelblogs.co.uk.WorkflowActivities.AudienceActivity
{
    /// <summary>
    /// Windows Workflow Activity for SharePoint 2010. Checks if
    /// Initiating User is a member of the specified Audience.
    /// Written by Joel Jeffery, 2011-10-28.
    /// </summary>
    class AudienceMemberActivity : Activity
    {
        /// <summary>
        /// Returns whether the user exists in the specified audience or not
        /// -- signature to match SharePoint Designer Requirement
        /// </summary>
        /// <param name="workflowContext">Environment for activity</param>
        /// <param name="listId">ID of the list the workflow is running on (unused)</param>
        /// <param name="itemId">Item ID of the item the workflow is running on (unused)</param>
        /// <param name="siteUrl">The audience name to determine whether the user is in it or not</param>
        /// <returns>True if site exists, false if not </returns>
        public static bool IsUserMemberOfAudienceCondition(
            WorkflowContext workflowContext, string listId, int itemId, string audienceName)
        {
            string exception;
            return (IsUserMemberOfAudience(
                workflowContext.InitiatorUser.LoginName, audienceName, out exception));
        }

        /// <summary>
        /// Determines whether [is user member of audience] [the specified login name].
        /// </summary>
        /// <param name="loginName">Name of the login.</param>
        /// <param name="audienceName">Name of the audience.</param>
        /// <param name="exception">The exception.</param>
        /// <returns>
        ///   <c>true</c> if [is user member of audience] [the specified login name]; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsUserMemberOfAudience(string loginName, string audienceName, out string exception)
        {
            try
            {
                exception = null;
                SPServiceContext context = SPServiceContext.Current;
                AudienceManager audManager = new AudienceManager(context);
                return audManager.IsMemberOfAudience(loginName, audienceName);
            }
            catch (Exception e)
            {
                exception = e.ToString();
                return (false);
            }
        }

        public static DependencyProperty AudienceNameProperty =
            DependencyProperty.Register("AudienceName", typeof(string), typeof(AudienceMemberActivity));

        [Description("The absolute URL of the site or site collection to create")]
        [Browsable(true)]
        [Category("joelblogs.co.uk Activities")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public string SiteUrl
        {
            get { return ((string)base.GetValue(AudienceNameProperty)); }
            set { base.SetValue(AudienceNameProperty, value); }
        }

        public static DependencyProperty ExistsProperty =
            DependencyProperty.Register("Exists", typeof(bool), typeof(AudienceMemberActivity));
        [Description("The result of the operation indicating whether the site exists or not")]
        [Browsable(true)]
        [Category("joelblogs.co.uk Activities")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public bool Exists
        {
            get { return ((bool)base.GetValue(ExistsProperty)); }
            set { base.SetValue(ExistsProperty, value); }
        }

        public static DependencyProperty ExceptionProperty =
            DependencyProperty.Register("Exception", typeof(string), typeof(AudienceMemberActivity));
        [Description("The exception generated while testing for the existance of the site")]
        [Browsable(true)]
        [Category("joelblogs.co.uk Activities")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public string Exception
        {
            get { return ((string)base.GetValue(ExceptionProperty)); }
            set { base.SetValue(ExceptionProperty, value); }
        }
    }
}

 

You’ll also need a terribly clever feature receiver implementation from the SPG that uses the SPWebConfigModification class to add AuthorizedType blocks to the web.configs throughout our farm, or our class won’t be loaded by WF.

using System;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

namespace joelblogs.co.uk.WorkflowActivities.AudienceActivity.Features.AudienceTestActivity
{
    /// <summary>
    /// This class handles events raised during feature activation, deactivation, 
    /// installation, uninstallation, and upgrade.
    /// </summary>
    /// <remarks>
    /// The GUID attached to this class may be used during packaging and should not be modified.
    /// </remarks>

    [Guid("a91d2258-b39b-4ca4-8282-2565c061378d")]
    public class AudienceTestActivityEventReceiver : SPFeatureReceiver
    {
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPWebService contentService = SPWebService.ContentService;
                contentService.WebConfigModifications.Add(GetConfigModification());
                // Serialize the web application state and propagate changes across the farm. 
                contentService.Update();
                // Save web.config changes.
                contentService.ApplyWebConfigModifications();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
                throw;
            }
        }

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPWebService contentService = SPWebService.ContentService;
                contentService.WebConfigModifications.Remove(GetConfigModification());
                // Serialize the web application state and propagate changes across the farm. 
                contentService.Update();
                // Save web.config changes.
                contentService.ApplyWebConfigModifications();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
                throw;
            }
        }

        public SPWebConfigModification GetConfigModification()
        {
            string assemblyValue = typeof(AudienceMemberActivity).Assembly.FullName;
            string namespaceValue = typeof(AudienceMemberActivity).Namespace;

            SPWebConfigModification modification = new SPWebConfigModification(
                string.Format(CultureInfo.CurrentCulture,
                    "authorizedType[@Assembly='{0}'][@Namespace='{1}']" +
                    "[@TypeName='*'][@Authorized='True']", assemblyValue, namespaceValue),
                "configuration/System.Workflow.ComponentModel.WorkflowCompiler/authorizedTypes");

            modification.Owner = "joelblogs.co.uk";
            modification.Sequence = 0;
            modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
            modification.Value =
                string.Format(CultureInfo.CurrentCulture,
                "<authorizedType Assembly=\"{0}\" Namespace=\"{1}\" " +
                "TypeName=\"*\" Authorized=\"True\" />", assemblyValue, namespaceValue);

            Trace.TraceInformation("SPWebConfigModification value: {0}", modification.Value);

            return modification;
        }
    }
}

You can download the full source code to my SharePoint 2010 Audience Membership Workflow Activity (Full Trust) here: http://spamwaft.codeplex.com.

Technorati Tags: SharePoint, SharePoint 2010, SharePoint Designer 2010, SharePoint Development, Workflow

How Does the Recycle Bin Work in SharePoint 2010?

I was recently asked how the dual stage recycle bins worked in SharePoint 2010 by one of my students (thank you, Andrew!), and I realised that I didn’t know enough. It’s not just me either. This has to be one of the poorest-documented SharePoint features on the Internet.

This article charts my journey of discovery.

Introduction

The SharePoint Recycle Bin is a complex entity. It has two stages, both accessible from within the site collection.

The 1st stage, known as the “end user recycle bin”, is where lists, documents and list items delete by users are sent. Their storage limit is known to be included in whatever site collection quota.

The 2nd stage, (accessibly only by site collection administrators) known as the “deleted from end user recycle bin”, is where such documents are sent when the user deletes them from the recycle bin. Their storage limit is known to be a percentage on top of the site collection quota, and is specified as a single setting, that applies to a whole web application.

Objectives

This experiment aims to show through empirical means how the SharePoint 2010 1st and 2nd Stage Recycle Bins behave.

Hypothesis

It is commonly understood that when the 1st stage time-to-live is reached, the deleted items will migrate to the 2nd stage recycle bin, to be held their indefinitely, or until the percentage of space of the site collection quota has been reach.

It is documented on Microsoft TechNet that the Recycle Bin timer job runs daily to find deleted content and “moves it to the next stage or deletes it”:

image_thumb23

The behaviour is further documented on p68 of the Microsoft Press book “SharePoint 2010 Administrator’s Companion” by Bill English:

image_thumb24

The TechNet page “Plan to protect content by using recycle bins” is ambiguous in its wording, but can be read as contradicting the other two sources.

Firstly…

When a user does this The item is The item can be restored by

Deletes an item

Held in the first-stage Recycle Bin until the item is deleted from the Recycle Bin or the item has been in the Recycle Bin longer than the time limit configured for an item to be held in the Recycle Bin.

Users or site collection administrators

Deletes an item from the Recycle Bin

Held in the second-stage Recycle Bin

Site collection administrators

And secondly…

The time limit for the Recycle Bins applies to the total time after the item was first deleted — not the time spent in either Recycle Bin stage.

Method

This experiment was carried out with SharePoint 2010 SP1 and the July 2011 Cumulative Update.

Two documents were placed into a Shared Documents library.

image_thumb17

These documents were then deleted within 60 seconds of each other. Next, within a further 60 seconds, the second document was deleted from the 1st stage recycle bin. Its presence in the 2nd stage bin was then confirmed.

The site collection had a quota of 100MB.

image_thumb22

The web application recycle bin time to live was adjusted down to 1 day, and the system clock was advanced by 24 hours.

image_thumb11

The “Recycle Bin” timer job in Central Administration was then forced to run.

image_thumb9

Observations

After the Recycle Bin timer job completed, both the 1st and 2nd stage recycle bins were found to be empty.

image_thumb1

Nothing.

image_thumb7

Not a sausage.

Conclusions

  1. There is a timer job that empties recycle bins. If this does not run, nothing ever gets deleted.
  2. The timer job deletes from 1st and 2nd stage bins anything that is older than the retention age FROM THE DATE FIRST DELETED, up to the maximum size allowed through quotas.
  3. The only way a deleted item can get into the 2nd stage bin is if a user deletes it from the 1st stage recycle bin.
  4. You really need a file-level backup and restore solution.
  5. Even really clever people don’t understand how this works.

Technorati Tags: Recycle Bin, SharePoint 2010, SharePoint Administration

SharePoint 2010 Development Machine Spec

If you’re considering buying a new developer laptop or desktop for SharePoint 2010 development, I can thoroughly recommend getting the fastest and most capacious build you can justify.

Development Laptop Build

Here’s my laptop build, and is currently what I consider to be minimum developer spec for serious SharePoint 2010 development work.

Model: Dell XPS L702

CPU: Intel i7-2720QM

RAM: 16GB

Disk: 500GB SSD (Kingston), 500GB eSata

Operating Systems

I run Windows 7 as a native OS on my laptop. This means I can code whilst on the train, and cut-and-run without creating a boil-in-the-bag laptop. I like this feature.

This is great for ad hoc development, but for more formal scenarios, I have a Windows 2008R2 SP1 native boot virtual hard disk (.vhd) added to my boot menu. When I boot into it, I have the Hyper-V role enabled which means I can run my choice of server platforms to more accurately mimic the customer’s environment. The .vhd files loaded by Hyper-V are kept natively on the SSD drive.

For some projects that require a unique, but standalone server build, I have Oracle VirtualBox installed under Windows 7. This means I can have a functioning Windows 7 machine for email and casual use wrapped around a dedicated single Windows Server VM that has the customer environment and developer tools installed.

For the most power-hungry builds, I use another virtual native boot .vhd, with Windows Server 2008R2 SP1 and the development tools natively installed.

Technorati Tags: SharePoint 2010, SharePoint Development

Renaming a Standalone SharePoint 2010 Server

OK. You never create standalone servers. I know. But, let’s just say you *did* have one, maybe for development or test, and let’s say you had such an environment on your laptop, and also let’s say your machine had an embarrassing name that you needed to change before you next demoed SharePoint stuff on it… OK, you get the picture. Smile

The Microsoft recommended steps are:

  1. Rename your PC using Computer properties:
    Renaming your PC
  2. Reboot.
  3. Run the PowerShell Command “Rename-SPServer

There is only one problem with this suggested method: it’s a steaming pile of nonsense.

The correct procedure would be:

  1. Rename your server using “Rename-SPServer”
  2. Rename the PC using Computer properties
  3. Reboot
  4. Rename your SQL instances
  5. Restart SQL
  6. iisreset /noforce

If you are unlucky enough to have tried to follow the official guidelines and renamed the PC before executing Rename-SPServer, you would get a message like this when trying to launch the SharePoint Management Shell:

The local farm is not accessible. Cmdlets with FeatureDependencyId are not registered.

Then the correct recovery would be:

  1. Rename the server using “stsadm -o renameserver
  2. iisreset /noforce
  3. Rename your SQL instances.
  4. Restart SQL
  5. iisreset /noforce

Best of luck!

Technorati Tags: PowerShell, SharePoint, SharePoint 2010, SharePoint Administration