Posts tagged ‘sharepoint’

November 13, 2013

Operation ShareLove – Help Typhoon Haiyan victims and SharePoint Experts will help you

Dux Raymond Sy, an MVP in the SharePoint community has come up with a unique and brilliant idea to encourage people to help support the Typhoon Haiyan victims at this time. You can read more about his response to this tragedy at – http://meetdux.com/2013/11/12/operation-sharelove-help-typhoon-haiyan-victims-sharepoint-experts-will-help-you-rescueph/

If you’re a SharePoint expert please think about giving your time to support his efforts, and if you are a company/individual, please do give to the cause.

Advertisements
Tags:
April 14, 2011

Access Denied error for pages with SummaryLinkWebPart

If you are getting an Access Denied error for pages with a (potentially empty) Summary Link Web Part on it, on WSS 3 or MOSS2007, do check that the permissions on the Style Library for the current site collection allow read access for the group that the user, getting the error, is in.

You may see List guids appended to the end of the Access Denied page URL, but these MAY NOT be directly at fault, and it’s worth a quick check to ensure that the Style Library has the correct permissions, as the XSL Style Sheets for the Summary Link Web Part are used directly from this web part.

October 21, 2010

Versioning attachments in a SharePoint list – an implementation

Further to the post on the 14th of October, here is a complete implementation of versioning attachments in a SharePoint list as a Visual Studio 2010 project.

Once the .wsp has been built and deployed, it can be activated at the web scope. Once this is done, on all lists an item called “List attachment versioning” appears on the List Settings page. When accessed this allows the user to switch attachment versioning on or off. This in turn will create a hidden document library and hidden fields as necessary to support the attachment versioning.

Please do try it and let me know what you think.

Download the solution here.

Update: 17/11/2011 – Error fixed where the solution only works on a root site.

October 14, 2010

Versioning attachments in a SharePoint list

One drawback that SharePoint lists have is that attachments are not versioned along with other changes to a list item when list versioning is enabled. This means that certain circumstances are made more complex than they need to be in grouping multiple documents that are actually related to a list item, rather than classified by the meta data within the list item.

In order to overcome this in SharePoint 2010 there are a few things we must do. We need to attach receivers to the ItemAdding, ItemUpdating, ItemAttachmentAdding and ItemAttachmentDeleting methods. These will be used to push the attachments to a document library. Since we will use a document library to store the files, we will need to provision a document library. Finally we will need a custom number field we can use to store the current version of the ListItem. We need to do this because there is no way from the properties object in a receiver method to discover whether we are involved in a rollback. Therefore we can use a custom number field which we increment in the ItemAdding and ItemUpdating methods to store which version we are at. When a rollback occurs this field would also be attempted to be rolled back. We can detect this in the ItemUpdating receiver and therefore pull back documents which were at this version.

So to start with we initially create a Site Column with a Type of “Number”, and include this in our Content Type and List Definition. In addition we can create a ListInstance from this content type and a ListInstance of a document library, where the documents will be persisted. We will also need a custom Site Column of Type “Text” to hold the version numbers the attachment is associated with. We declare this with our normal Field and ListInstance syntax, using ContentTypeBinding to bind our content types to the lists we have created.

Next we need to create the receiver, as defined above. We can do this with a class like the following:

    /// <summary>
    /// Handles the redirection of attachments to a document library
    /// </summary>
    public class RedirectAttachmentsItemEventReceiver : SPItemEventReceiver
    {
        /// <summary>
        /// Local HTTP Context cache
        /// </summary>
        private HttpContext context;

        private string targetDocumentLibrary;
        private string targetLookupField;

        /// <summary>
        /// HTTP Context is available in creation
        /// </summary>
        public RedirectAttachmentsItemEventReceiver()
            : base()
        {
            context = HttpContext.Current;
        }

        /// <summary>
        /// Parses the receiver data string into private variables
        /// </summary>
        /// <param name="receiverData"></param>
        private void ParseReceiverData(string receiverData)
        {
            this.targetDocumentLibrary = receiverData.Split(';')[0];
            this.targetLookupField = receiverData.Split(';')[1];
        }

        /// <summary>
        /// Handles the adding of an attachment to the list
        /// Adds it into the document library
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemAttachmentAdding(SPItemEventProperties properties)
        {
            base.ItemAttachmentAdding(properties);
            int attachmentNumber = 0;
            if (int.TryParse(SPContext.GetContext(context).Web.Properties["attachmentNumber"], out attachmentNumber))
            {
                attachmentNumber++;
                SPContext.GetContext(context).Web.Properties["attachmentNumber"] = attachmentNumber.ToString();
                ParseReceiverData(properties.ReceiverData);
                string fileName = properties.AfterUrl.Split('/')[4];
                //Put the item into the document library
                using (SPWeb web = properties.OpenWeb())
                {
                    SPList documentLibrary = web.Lists[targetDocumentLibrary];
                    SPFolder targetFolder;
                    try
                    {
                        targetFolder = documentLibrary.RootFolder.SubFolders[properties.ListItemId.ToString()];
                    }
                    catch
                    {
                        targetFolder = documentLibrary.RootFolder.SubFolders.Add(properties.ListItemId.ToString());
                    }
                    SPFile document;
                    try
                    {
                        document = targetFolder.Files[fileName];
                    }
                    catch (ArgumentException)
                    {
                        document = targetFolder.Files.Add(fileName, context.Request.Files[attachmentNumber - 1].InputStream);
                    }
                    if (document.Item["VersionLabel"] != null)
                        document.Item["VersionLabel"] = document.Item["VersionLabel"].ToString() + ";" + properties.ListItem.Versions[0].VersionLabel + ";";
                    else
                        document.Item["VersionLabel"] = ";" + properties.ListItem.Versions[0].VersionLabel + ";";
                    document.Item[targetLookupField] = new SPFieldLookupValue(properties.ListItem.ID, properties.ListItem.Title);
                    document.Item.Update();
                }
            }
        }

        /// <summary>
        /// Handles the deleting of an attachment from the list
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemAttachmentDeleting(SPItemEventProperties properties)
        {
            base.ItemAttachmentDeleting(properties);
            ParseReceiverData(properties.ReceiverData);
            int nextVersion = (int)float.Parse(properties.ListItem["CustomVersion"].ToString()) + 1;
            string fileName = properties.BeforeUrl.Split('/')[4];
            //Put the item into the document library
            using (SPWeb web = properties.OpenWeb())
            {
                SPList documentLibrary = web.Lists[targetDocumentLibrary];
                SPFolder targetFolder;
                try
                {
                    targetFolder = documentLibrary.RootFolder.SubFolders[properties.ListItemId.ToString()];
                }
                catch
                {
                    targetFolder = documentLibrary.RootFolder.SubFolders.Add(properties.ListItemId.ToString());
                }
                SPFile document = targetFolder.Files[fileName];
                if (document.Item["VersionLabel"] != null)
                    document.Item["VersionLabel"] = document.Item["VersionLabel"].ToString().Replace(";" + nextVersion.ToString() + ".0;", "");
                document.Item.Update();
            }
        }

        /// <summary>
        /// Set the custom version property
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemAdding(SPItemEventProperties properties)
        {
            base.ItemAdding(properties);
            ParseReceiverData(properties.ReceiverData);
            properties.AfterProperties["CustomVersion"] = 1;
            SPContext.GetContext(context).Web.Properties["attachmentNumber"] = "0";
        }

        /// <summary>
        /// Update the custom version property
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemUpdating(SPItemEventProperties properties)
        {
            base.ItemUpdating(properties);
            SPContext.GetContext(context).Web.Properties["attachmentNumber"] = "0";
            ParseReceiverData(properties.ReceiverData);
            int currentVersion = (int)float.Parse(properties.ListItem["CustomVersion"].ToString());
            int newVersion;
            if (properties.AfterProperties["CustomVersion"] == null)
                newVersion = currentVersion;
            else
                newVersion = (int)float.Parse(properties.AfterProperties["CustomVersion"].ToString());
            int nextVersion = currentVersion + 1;
            using (SPWeb web = properties.OpenWeb())
            {
                SPList documentLibrary = web.Lists[targetDocumentLibrary];
                SPFolder targetFolder;
                try
                {
                    targetFolder = documentLibrary.RootFolder.SubFolders[properties.ListItemId.ToString()];
                }
                catch
                {
                    targetFolder = documentLibrary.RootFolder.SubFolders.Add(properties.ListItemId.ToString());
                }
                if (currentVersion > newVersion)
                {
                    EventFiringEnabled = false;
                    //Clear the list items attachments
                    List<string> attachmentsToDelete = new List<string>();
                    foreach (string attachmentName in properties.ListItem.Attachments)
                    {
                        attachmentsToDelete.Add(attachmentName);
                    }
                    foreach (string attachmentName in attachmentsToDelete)
                    {
                        properties.ListItem.Attachments.Delete(attachmentName);
                    }
                    //Pull attachments for this version
                    SPQuery query = new SPQuery();
                    query.ViewAttributes = "Scope='Recursive'";
                    query.Query = String.Format("<Where><And><Contains><FieldRef Name='VersionLabel' /><Value Type='Text'>;{0}.0;</Value></Contains><Eq><FieldRef Name='{1}' /><Value Type='Lookup'>{2}</Value></Eq></And></Where>", newVersion, targetLookupField, properties.ListItem.Title);
                    SPListItemCollection items = documentLibrary.GetItems(query);
                    foreach (SPListItem item in items)
                    {
                        properties.ListItem.Attachments.Add(item.File.Name, item.File.OpenBinary());
                        item["VersionLabel"] = item["VersionLabel"].ToString() + ";" + nextVersion.ToString() + ".0;";
                        item.SystemUpdate(false);
                    }
                    properties.ListItem.SystemUpdate(false);
                    EventFiringEnabled = true;
                }
                else
                {
                    //Update all attachments
                    foreach (string fileName in properties.ListItem.Attachments)
                    {
                        SPFile document = targetFolder.Files[fileName];
                        document.Item["VersionLabel"] = document.Item["VersionLabel"].ToString() + ";" + nextVersion.ToString() + ".0;";
                        document.Item.Update();
                    }
                }
            }
            properties.AfterProperties["CustomVersion"] = nextVersion;
        }
    }

In the class above we can see we are using the ReceiverData field which we will define below, when attaching the event receiver, to store the document library and the name of the lookup field in the content type that we will be using to link back. We are creating a folder per ID of the item in the master list, and using this folder to store all attachments against any version of this item. We are tracking the version on the master list, and can, using the ListItem values and the AfterProperties values deduce from this whether we are doing a rollback or not.

Finally we need to attach this to our list and let it know which document library to point at using the following syntax:

  <Receivers ListUrl="Lists/My List">
    <Receiver>
      <Name>RedirectAttachmentsItemEventReceiver</Name>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Ebenezer.RedirectAttachments.EventReceivers.RedirectAttachmentsItemEventReceiver</Class>
      <Type>ItemAttachmentAdding</Type>
      <SequenceNumber>10000</SequenceNumber>
      <Data>My List Attachments;My List</Data>
    </Receiver>
    <Receiver>
      <Name>RedirectAttachmentsItemEventReceiver</Name>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Ebenezer.RedirectAttachments.EventReceivers.RedirectAttachmentsItemEventReceiver</Class>
      <Type>ItemUpdating</Type>
      <SequenceNumber>10001</SequenceNumber>
      <Data>My List Attachments;My List</Data>
    </Receiver>
    <Receiver>
      <Name>RedirectAttachmentsItemEventReceiver</Name>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Ebenezer.RedirectAttachments.EventReceivers.RedirectAttachmentsItemEventReceiver</Class>
      <Type>ItemAdding</Type>
      <SequenceNumber>10002</SequenceNumber>
      <Data>My List Attachments;My List</Data>
    </Receiver>
    <Receiver>
      <Name>RedirectAttachmentsItemEventReceiver</Name>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Ebenezer.RedirectAttachments.EventReceivers.RedirectAttachmentsItemEventReceiver</Class>
      <Type>ItemAttachmentDeleting</Type>
      <SequenceNumber>10003</SequenceNumber>
      <Data>My List Attachments;My List</Data>
    </Receiver>
  </Receivers>

The advantage of this method is that no changes are needed to the out of the box edit/new forms in order for the attachments to be correctly versioned and updated with rollbacks.

September 29, 2010

Using FieldRenderingControls in custom web forms

Within a custom Web Form we can add references to Microsoft.SharePoint.WebControls and add SharePoint FormField controls to render controls to edit values within a list. We can declare a FormField as follows:

<SharePoint:FormField ID="field_id" runat="server" FieldName="MyField" ControlMode="New" />

However, in addition to this definition, the FormField needs to know the list and content type, in case of being a New form, that it is attaching to. This can be attached through the ItemContext property of FormField, which uses an SPContext object (defaulting to SPContext.Current). Of course when we are in a Web Form, SPContext.Current will not be attached to a List or to a particular Item. Therefore, if we want this field to be able to work against an item we need to change the context that is attached to it.

We can achieve this by getting a context using SPContext.GetContext with the web and list we want in mind, and attaching this to our field, using something like the following code,

using (SPWeb web = SPContext.Current.Site.OpenWeb(webLocation))
{
	SPList targetList = web.Lists[listName];
	SPContext context = SPContext.GetContext(HttpContext.Current, 0, targetList.ID, web);
	field_id.ItemContext = context;
}

As can be seen, for the item id parameter, at the moment we have passed in 0, which should, in theory, work. However, when we add a SharePoint:SaveButton control to the form and try with this, we will see an exception is raised.

To get around this, under the context declaration we need to explicitly state the FormMode (this is set automatically when the item id is greater than 0 to Edit, but in the case of 0 is left as Invalid). We can do this with the code context.FormContext.FormMode = SPControlMode.New.

The context ideally needs to be attached to the element directly after it is added to its parent control. To achieve this in a clean way we can override the AddedControl method, which is called when any control is added to another control. We can then recurse over all controls in that control set and if it is a FieldMetadata (the base class for BaseFieldControl) derived class, or FormComponent (the base class for items such as SaveButton) derived class, set the item context.

protected override void AddedControl(Control control, int index)
{
	base.AddedControl(control, index);
	SetControlContext(control);
}

protected void SetControlContext(control)
{
	if (control is FieldMetadata)
	{
		FieldMetadata field = control as FieldMetadata;
		field.ItemContext = this.SPContext;
		field.ControlMode = this.SPContext.FormContext.FormMode;
	}
	if (control is FormComponent)
	{
		FormComponent formComponent = control as FormComponent;
		formComponent.ItemContext = this.SPContext;
		formComponent.ControlMode = this.SPContext.FormContext.FormMode;
	}
	if (control.HasControls)
	{
		foreach (Control childControl in control.Controls)
		{
			SetControlContext(childControl);
		}
	}	
}

There are two things of note above, firstly, SPContext is a property of the class in which this sits. We do this, so that the context can be instantiated once, and then reused. Secondly, the field’s ControlMode is being set from the Context, this gives us the freedom of having a form that is both an Add form or an Edit form based on the query string parameters sent to the page. We can parse these parameters inside the SPContext property to set the ControlMode of the underlying controls. In addition this means our controls can lose the ControlMode property, and therefore their definition becomes less verbose.

We could therefore, finally, declare our property as follows:

private SPContext cachedContext = null;

protected SPContext SPContext
{
	get
	{
		if (cachedContext == null)
		{
			using (SPWeb web = SPContext.Current.Site.OpenWeb(webUrl))
			{
				SPList targetList = web.Lists[listName];
				int itemId = 0;
				int.TryParse(Response.QueryString("itemId"), out itemId);
				cachedContext = SPContext.GetContext(HttpContext.Current, itemId, targetList.ID, web);
				if (itemId &gt; 0)
				{
					cachedContext.FormContext.FormMode = Edit;
				}
				else
				{
					cachedContext.FormContext.FormMode = New;
				}								
			}
		}
		return cachedContext;
	}
}

We can wrap all this code inside a simple Control which will set the properties of all sub controls and in this way achieve custom data entry forms using web forms techniques.

September 27, 2010

Failed to create receiver object SharePoint exception

When developing with SharePoint 2010, I had a working project with multiple features, one of which had an Feature Receiver added, working absolutely fine. However, on adding an Feature Receiver to a second feature in the same project, I encountered the following error:

Failed to create receiver object from assembly "AssemblySignature", class "EventReceiverClassName"
System.ArgumentNullException: Value cannot be null. Parameter name: type

After first checking that the Receiver name was correctly defined in Visual Studio the solution ended up being far more simple than I could have anticipated – by simplying manually retracting and removing the offending wsp from the farm solution store, and then redeploying, the problem went away.

September 16, 2010

Provisioning taxonomy type site columns in a feature

Taxonomy type site columns give us the same issues as Lookup type site columns have historically, with a slight twist.

Where, with Lookup fields, we can specify all the attributes in the XML correctly but they don’t get picked up correctly; with Taxonomy fields we don’t have attributes for all the necessary pieces of information – particularly the TermSetId and the SspId (Id of the Term Store).

To get round this issue, we can use a similar method to that which we used for Lookup fields in MOSS 2007, whereby we attached a feature receiver to correctly set the List GUID based on the List name we had specified in the CAML field definition.

To this end we firstly need some CAML in our Elements file, to provision the Site Column

<Field ID="{5d0d24ee-c332-465d-8efc-83d40ab615ff}"
         Name="MyTaxonomyField"
         Group="My Fields"
         DisplayName="My Taxonomy Field"
         Type="TaxonomyFieldTypeMulti"
         Required="FALSE">
    <Customization>
      <ArrayOfProperty>
        <Property>
          <Name>SspName</Name>
          <Value>Managed Metadata Service</Value>
        </Property>
        <Property>
          <Name>TermGroup</Name>
          <Value>Keywords Group</Value>
        </Property>
        <Property>
          <Name>TermSet</Name>
          <Value>Keywords</Value>
        </Property>
        <Property>
          <Name>TextFieldId</Name>
          <Value>700a3d33-a85c-47dd-80d1-2eba10439b46</Value>
        </Property>
      </ArrayOfProperty>
    </Customization>
  </Field>

As can be seen, we have defined some additional custom properties to set the service application name, the termstore group, and the termset which we want the field to access. We must do this since there aren’t any attributes in the schema that we can use for these purposes.

In addition, Taxonomy Fields require a note type field alongside them. Above, the name can be seen stored as a custom property, so in order for this to work, we need to define this site column in CAML as follows:

<Field ID="{700a3d33-a85c-47dd-80d1-2eba10439b46}"
         Name="MyTaxonomyFieldText"
         Group="My Fields"
         Hidden="TRUE"
         DisplayName="My Taxonomy Field Text"
         Required="FALSE"
         ShowInNewForm="FALSE"
         Type="Note" />

We now need to write a feature receiver for the Site scoped feature which holds this solution element. Inside the FeatureActivated method, we want to traverse the Xml of our Elements file, and detect where the type of field is TaxonomyFieldType.

When we have that we can then update the column so that its definition is correct as follows (XML references as XPath, replace with whichever method you use – XmlDocument, XDocument, XPathDocument etc.)

//Get the current field

TaxonomyField taxonomyField = web.Fields[Field/@ID] as TaxonomyField;

TaxonomySession taxonomySession = new TaxonomySession(||Current site from properties.Feature.Parent||);

string sspName = {Property[Name='SspName']/Value}
string groupName = {Property[Name='TermGroup']/Value}
string termSetName = {Property[Name='TermSet']/Value}
string textFieldId = {Property[Name='TextFieldId']/Value}

//Set the term store id
taxonomyField.SspId = session.TermStores[sspName].Id;

//Set the term set id
taxonomyField.TermSetId = session.TermStores[sspName].Groups[groupName].TermSets[termSetName].Id;

//Set the text field's id
taxonomyField.TextField = new Guid(textFieldId);

//Save the changes
taxonomyField.Update();

Note: TaxonomyField and TaxonomySession classes used above are from the Microsoft.SharePoint.Taxonomy assembly/namespace, located in 14/ISAPI; you will also need to import System.Text.RegularExpressions

When this is run the Site Column will be correctly provisioned against the Managed Metadata Service. We can substitute in different values for this to work against alternative Service Applications, Groups and Term Sets.