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

Category : Development

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

After much persuasion, I’ve decided to fly in the face of public opinion and record one of the songs from my SharePoint show Smile

Stop, Collaborate and Listen:
SharePoint’s Back with a Brand New Invention.

Technorati Tags: Comedy, Development, SharePoint, SharePoint 2010 Training, Songs, Videos

I was recently asked how to remove the Editor Parts (Appearance, Layout, Advanced, etc) from custom Web Parts.

One strategy is to implement your own Editor Part and mark the built-in ones as not Visible.

Firstly, we need to create our Editor Part class:

class MyEditorPart : EditorPart
{
    protected override void CreateChildControls()
    {        // this line hides the default EditorParts
        Parent.Controls[2].Visible = false;
        base.CreateChildControls();
    }

    public override bool ApplyChanges()
    {
        // do stuff here
        return true;
    }

    public override void SyncChanges()
    {
        // do stuff here
    }
}

And here’s how we invoke our Editor Part from our Web Part:

public class MyWebPart : WebPart
{
    public override EditorPartCollection CreateEditorParts()
    {
        ArrayList aryParts = new ArrayList();

        MyEditorPart myEditor = new MyEditorPart();
        myEditor.ID = this.ID + "_myEditorPart";
        aryParts.Add(myEditor);

        return new EditorPartCollection(aryParts);
    }
    // do more stuff here...
}

Hopefully, when you edit the Web Part it should look something like this:

The next step would be to extend CreateChildControls(), ApplyChanges() and SyncChanges() to get and set any properties you’d like to allow users to edit.

Technorati Tags: Development, SharePoint, SharePoint 2010 Training, SharePoint Developer, SharePoint Development, WebParts

SharePoint 2010 in Visual Studio 2010 has made creating Windows Workflow Foundation (WF) workflows an awful lot easier. There are still one or two areas that can be confusing to new developers

One such region of confusion surrounds the Initiation Form – the form displayed whenever a user launches your workflow, that you can use to prompt for more information required to run your workflow. Similar uncertainty surrounds the Association Form – one that is displayed whenever a list administrator attaches your workflow to their list.

Thanks to the tooling in Visual Studio, it’s now easy to add either of these forms. Right-clicking your Workflow item in Solution Explorer and choosing “Add-> New Item” will give you:

Adding a New SharePoint Item in Visual Studio 2010

Selecting either Initiation Form or Association Form will add the appropriate artefacts to your code and also configures the Element manifest of the Workflow accordingly:

Element Manifest Changes to view Initiation Forms

Let’s say we wanted to ask the user a series of extra questions when they launch our workflow, such as their manager’s First Name, Last Name and Email Address, such that we can have access to that data within our running workflow later on.

We can now edit our new Initiation Form as an aspx page in Visual Studio. Let’s add some text boxes to capture the extra information:

image

The next question is often “how do we get this information to the workflow?” Inside the workflow itself is a SPWorkflowActivationProperties object which exposes two strings: InitiationData and AssociationData, which is respectively the data captured from the user during launch and association phases of the workflow.

The code behind for the Initiation Form has a string method called GetInitiationData(). Whatever string you return here is then available from inside your workflow using workflowProperties.InitiationData. It’s the same principle with Association Forms, GetAssociationData and AssociationData properties.

Clearly, we could go low-rent here and return a semi-colon delimited string or something naff of that ilk.

Alternatively you could store your properties temporarily in a class and then serialise that class to a string and return that. Then later in your workflow you could deserialise that back to an instance of your object.

You could put a lot of effort into this to get it really efficient, but the general principle is why not create a utility class to hold Serialise and Deserialise methods?

I’ve implemented the following candidate code using Generics to show how powerful the technique is:

public static string Serialise<T>(T item)
{
    XmlSerializer ser = new XmlSerializer(item.GetType());
    TextWriter sw = new StringWriter();
    ser.Serialize(sw, item);
    return sw.ToString();
}

public static T Deserialise<T>(string xml) where T : new()
{
    T returnObject = new T();
    XmlSerializer ser = new XmlSerializer(returnObject.GetType());
    XmlTextReader xtr = new XmlTextReader(new StringReader(xml));
    return (T)ser.Deserialize(xtr);
}

So, given a class, for example, to hold contact information:

public class Contact
{
    public string FirstName;
    public string LastName;
    public string Email;
}

You could put the following in your code behind for the Initiation Form:

// This method is called when the user clicks the button to start the workflow.
private string GetInitiationData()
{
    contact.FirstName = firstName.Text;
    contact.LastName = lastName.Text;
    contact.Email = email.Text;
    return Serialise<Contact>(contact);
}

Then in the code beside for your workflow you can get at the data again like this:

private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
    Contact contact = Deserialise<Contact>(workflowProperties.InitiationData);
    historyDescription = string.Format("Received Contact information: {0} {1} {2}",
        contact.FirstName, contact.LastName, contact.Email);
}

Hopefully this code will save you some searching and typing.

Technorati Tags: SharePoint, SharePoint 2010, SharePoint 2010 Training, SharePoint Developer