Second Life of a Hungarian SharePoint Geek

October 16, 2011

Customizing SharePoint forms via creating our own ListFieldIterator

Filed under: Custom forms, SP 2010 — Tags: , — Peter Holpar @ 21:42

Altering either the design or the behavior of the standard forms that are used to display, edit existing list items or create new ones is a frequent customer request.

In this post I introduce you a technique I apply daily for such tasks, and give a few examples of possible applications. In later posts I plan to provide examples for the most interesting ones.

The standard forms I’m talking about are DispForm.aspx, EditForm.aspx and NewForm.aspx. Of course, you can customize further your existing custom forms as well using the very same technique.

Create an Empty SharePoint Project in Visual Studio 2010.

Create a SharePoint mapped folder that points to the ControlTemplates folder.

Right click on the ControlTemplates folder in Solution Explorer and select Open Folder in Windows Explorer.

Create a new Text Document file and rename it to MyCompany_MyListForm.ascx. Of course, you can use other file names either as long as you keep the file extension and the name is not conflicting with other files in 14\TEMPLATE\CONTROLTEMPLATES.

Drag’n drop MyCompany_MyListForm.ascx from Windows Explorer to the ControlTemplates folder in Solution Explorer of Visual Studio.

Open 14\TEMPLATE\CONTROLTEMPLATES\DefaultTemplates.ascx in a text editor.

Copy the header section of the user control file (all elements beginning with <%@) from DefaultTemplates.ascx and paste it to MyCompany_MyListForm.ascx in Visual Studio.

Add the following line to MyCompany_MyListForm.ascx (replace TagPrefix and namespace attributes with your ones if you wish, but keep them consistent with later changes in names):

<%@ Register TagPrefix="MyControls" Assembly="$SharePoint.Project.AssemblyFullName$" namespace="MyNameSpace"%>

Look up the RenderingTemplate having id="ListForm" in DefaultTemplates.ascx, select and copy this entire RenderingTemplate.

Close DefaultTemplates.ascx and if prompted, do not save any changes.

In MyCompany_MyListForm.ascx, paste the RenderingTemplate you copied earlier, and set its id attribute to MyListForm.

In MyCompany_MyListForm.ascx, look up this control:

<SharePoint:ListFieldIterator runat="server"/>

and replace it with this:

<MyControls:MyListFieldIterator runat="server"/>

Add a new Class to your SharePoint project (let’s call it MyListFieldIterator.cs), set the namespace to MyNameSpace and add:

using Microsoft.SharePoint.WebControls;

Alter the class definition as:

public class MyListFieldIterator : ListFieldIterator

We finished to create the base of the customized ListFieldIterator. Build and deploy the solution.

Using SharePoint Designer, select a site and list you would like to use with the new ListFieldIterator. Open the form you would like to alter. You can change any or all of DispForm.aspx, EditForm.aspx, NewForm.aspx.

Look up the ListFormWebPart in the Code view and add a new property TemplateName with the value set earlier as the id attribute of the RenderingTemplate to the WebPart:

<TemplateName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">MyListForm</TemplateName&gt;

Of course, if the TemplateName already exists there, you should update its value instead of creating a new one.

Note: Instead of using SPD you can deploy this settings from your SharePoint solution as well. For example, we typically deploy it using a custom feature receiver that utilize the SPLimitedWebPartManager and Reflection to set web part properties. I might be to blog about that later as my time allows.

Save the form in SPD and open the list using the web UI. Select an item to display / edit, or start to create a new item base. You should not see any difference now.

Switch back to VS, and add the text Hello World to MyCompany_MyListForm.ascx, just after the opening <Template> tag. Redeploy the solution and check the altered forms on the web UI. The Hello World message should be now visible.

At this point you may say it is much ado about nothing. I admit it is not the simplest way to display a message, but all we discussed earlier is only the base of a framework we can use later to achieve some really powerful results. Just to name some of them:

  • You can add custom activity to your form that is performed when the item is saved or specify what action to perform instead of saving. For example, you can cancel saving, send a mail, create a task, set permissions or even call external components.
  • You can interact with the list field controls and other controls on the form as well. You can hide some of the controls or set them to read-only mode even in the edit / new mode.
  • You can create custom field control arrangements in place of the standard one column multiple rows (one per field) form design.
  • Through your control you can insert a link to a custom JavaScript / jQuery file that adds dynamic behavior to your form.
  • You can interact with the ribbon from your custom ListFieldIterator, for example, you can disable ribbon elements based on your business requirements.
  • You can do custom validation. Although SharePoint 2010 provides validation at the form and the field level, both of these have their limitations. Sometimes you simply need to have multiple validation rules and error messages for a single form / field and you might need to validate field types not supported by the out of the box validation (lookup or user fields, managed metadata or even your custom field types).

If you read my post about custom field types vs. event receivers in the past, you can recognize some of the possibilities listed there, like this one about validation or this other one about hiding fields or displaying them as read-only in edit / new mode. It’s not a random coincidence, you can use both of these methods to achieve the same goals (after all, you can get a reference to the ListFieldIterator instance from the custom field and vice versa), however I recently prefer the technique of custom ListFieldIterator, simply because I found it is simpler and more powerful, easy to maintain and deploy, and probably fits better to the whole picture of form customization.

November 1, 2014

No more “Show More” in Tasks lists

Filed under: ListFieldIterator, PowerShell, SP 2013 — Tags: , — Peter Holpar @ 05:58

I meet frequently with the request to turn off the new “Show More” feature at the SharePoint 2013 Tasks lists. The next screenshot shows the default new task form as displayed after page load with the limited set of fields, the “Show More” button is highlighted:

image

After clicking on that button, the remaining fields are displayed, as shown below:

image

Surprisingly, the solutions I found on the web (like this, this or this one) try to solve the issue and expand the fields automatically on the client side using JavaScript – that I consider rather hacking – instead of solving the real reason of the problem.

In this blog post I would like to introduce a few other solutions to the problem, that are worth considering instead of the JavaScript approach.

As you probably know, the various list item forms (NewForm.aspx, DispForm.aspx and EditForm.aspx) use the ListFormWebPart web part to render the item in new, display and edit mode. Which rendering template the web part uses is specified by its TemplateName property. If no TemplateName is specified on the page itself for the web part, the default value is used:

image

As you can see from this code, the template is read from the configuration of the content type of the item being edited / displayed.

You can display this value in the case of a standard Tasks list using the next PowerShell script:

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["Tasks"]
$ct = $list.ContentTypes[0]
$ct.DisplayFormTemplateName

The output of this should be “TaskForm”, and you get the same result for the other two properties (NewFormTemplateName and EditFormTemplateName).

In the case of other list types the form used is the “ListForm”. So if you would like to use the standard form layout without the “Show More” button, you can simply replace the form template for the content type assigned to the list (solution 1).

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["Tasks"]
$ct = $list.ContentTypes[0]
$ct.DisplayFormTemplateName = "ListForm"
$ct.NewFormTemplateName = "ListForm"
$ct.EditFormTemplateName = "ListForm"
$ct.Update()

Note: The change affect only the given list, but no other Tasks lists, as we change the property only for the local copy of the Task content type.

Alternatively, you can open the form using SharePoint Designer, and set the TemplateName property of the ListFormWebPart web part explicitly (solution 2):

<TemplateName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">ListForm</TemplateName>

But what’s the difference between the TaskForm and ListForm templates? Don’t we lose any functionality if we simply switch the form template? What`s included in TaskForm and what in ListForm?

These questions can be answered if we have a look at these templates in DefaultTemplates.ascx (located in folder [SharePoint Root]\TEMPLATE\CONTROLTEMPLATES).

If we look for the templates having id="ListForm" and “TaskForm”, we find that there are several differences between them. Just to name a few, the ListForm uses a standard ListFieldIterator control, while in the TaskForm we find a TaskFieldIteratorSpecifiedListFieldIterator combo, and various combinations of an EditDatesSelector control. I had not yet time to investigate the purpose of the latter one, but having a look at the code of the TaskFieldIterator and its base class DividingListFieldIterator (via Reflector or dotPeek), this control itself seems powerful enough to find an other way to eliminate the “Show More” button.

In one of my former blog posts I’ve already described the process how can we customize a standard SharePoint rendering template. A altering an out-of-the-box file is definitely not recommended, you should create a new .ascx file (for example, call it CustomTaskForm.ascx) in the CONTROLTEMPLATES folder, and copy the content of the rendering template with id="TaskForm" into the file, and include the same Control, Assembly and Register headers as found in the DefaultTemplates.ascx. Alter the id property of the template to CustomTaskForm.

Add the ShowExpanded="true" attribute to the TaskListFieldIterator control in the file (solution 3).

<SharePoint:TaskListFieldIterator ShowExpanded="true" …

Save the changes and execute IISRESET.

Open the list item forms (NewForm.aspx, DispForm.aspx and EditForm.aspx) and assign the new custom rendering template to the ListFormWebPart.

<TemplateName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm"&gt;CustomTaskForm</TemplateName>

Save the changes again. The item should be displayed now with all its fields without the “Show More” button.

If you want to keep the button, but would like to include / exclude other fields into / from the top, you can change the value of the TopFields property. Mandatory fields of the content type and the fields specified in this property should be displayed by default without clicking on the “Show More” button.

For example, we could include the Priority field on the form, if we include ;#Priorty in this property.

<SharePoint:TaskListFieldIterator TopFields="…Title;#StartDate;#DueDate;#AssignedTo;#Priorty;#PercentComplete;#RelatedItems;#Description" runat="server"/>

Of course, in this case we should remove the ShowExpanded="true" we just included in the former step!

After saving the changes and an IISRESET, the forms should include the Priority field as well.

image

By studying the constructor of the TaskListFieldIterator class, we can find two further ways to show the form with all the fields.

We can pass the Expanded=1 in the request query string like NewForm.aspx?Expanded=1 (solution 4), or change the column order of the list content type, for example, switch the order of the Priority and Task Status fields (solution 5).

image

In both cases the form will be displayed with the fields expanded automatically. Of course, in the second case the field display order will differ from the standard one, but it’s only a minor difference.

image

I hope you find a method from the above described ones that fulfills your needs, and can easily eliminate the “Show More” button if you wish, without any kind of JavaScript magic.

November 14, 2011

Creating custom validation rules in our list field iterators

Filed under: CAML, Custom forms, SP 2010, Validation — Tags: , , , — Peter Holpar @ 13:36

Last week we saw how to hide specific field controls or how to display them as read-only even on the edit form. In the current post I introduce a simple technique one can apply to create custom validation for SharePoint forms without InfoPath.

First, what are the main problems with the out of the box SharePoint form validation capability?

As written in the first post of this series about custom list field iterators, SharePoint 2010 made a large step to the right direction compared to its predecessor, WSS 3.0. One can define field level and form level validation rules using a syntax similar to the one used with calculated columns, based on values of other fields of the current item.

However, you can define only a single validation rule for your form having a single validation error message, and a single validation rule and error message for the fields. Furthermore, there is another serious limitation, namely one can validate only the basic field types (like text, date, numeric) and the same is true to the fields one can refer to in the validation rule.

It means, you are out of luck if you have to work with validation of lookup or user fields, managed metadata or even your custom field types.

In the validation rule you can refer to the fields of the current item, you cannot build validation rules based on field values of other, related items, custom user properties, not to mention accessing other systems, like CRM, 3rd party web services, etc.

Moreover, the validation error messages are static text, one can not built them up dynamically based on field values, for example.

I found that in real world applications customers need usually much more validation power.  All of these inflexibilities listed above lead me to create some kind of custom solution for validation. You can read another approach built on custom field types here, but in this post I illustrate the list field iterator based solution.

As you might know the BaseFieldControl class, the base class of all field controls implements the IValidator interface. In practice it means, you can use these field controls similar to standard ASP.NET validator controls.

But before working with them, we first have to get a reference to the field controls. So we extend our BaseListFieldIterator class – introduced in the previous part – with the GetFieldControlByName method. There might be multiple forms on the page, so just to be sure that the field control is within our list field iterator, we call the GetIteratorByFieldControl method and compare the ClientID values.

The SetValidationError method is to set the validation error for the specified field.

  1. protected BaseFieldControl GetFieldControlByName(String fieldNameToFind)
  2. {
  3.     foreach (Control control in _formContext.FieldControlCollection)
  4.     {
  5.         if (control is BaseFieldControl)
  6.         {
  7.             BaseFieldControl baseField = (BaseFieldControl)control;
  8.             String fieldName = baseField.FieldName;
  9.             if ((fieldName == fieldNameToFind) &&
  10.                 (GetIteratorByFieldControl(baseField).ClientID == ClientID))
  11.             {
  12.                 return baseField;
  13.             }
  14.         }
  15.     }
  16.     return null;
  17. }
  18.  
  19. protected Microsoft.SharePoint.WebControls.ListFieldIterator GetIteratorByFieldControl(BaseFieldControl fieldControl)
  20. {
  21.     return (Microsoft.SharePoint.WebControls.ListFieldIterator)fieldControl.Parent.Parent.Parent.Parent.Parent;
  22. }
  23.  
  24. protected void SetValidationError(String fieldName, String errorMessage)
  25. {
  26.     BaseFieldControl fieldControl = GetFieldControlByName(fieldName);
  27.     fieldControl.ErrorMessage = errorMessage;
  28.     fieldControl.IsValid = false;
  29. }

Next we have to add the following code to our custom ListFieldIterator class.

  1. protected override void OnLoad(EventArgs e)
  2. {
  3.     if (Page.IsPostBack)
  4.     {
  5.         Page.Validate();
  6.         this.Validate();
  7.     }
  8. }
  9.  
  10. public void Validate()
  11. {
  12.     if (base.ControlMode != SPControlMode.Display)
  13.     {
  14.         // here comes the validation logic
  15.     }
  16. }

We will extend the Validate method to fulfill our fictitious business needs.

The validation messages are stored as constants:

  1. public static class ValidationMessages
  2. {
  3.     public static readonly String TitleNotUnique = "Task title should be unique";
  4.     public static readonly String UserBusy = "This user already has another task for this time";
  5.     public static readonly String NoDateForAssignement = "A user can be assigned only if the start and due dates are specified";
  6.     public static readonly String PredecessorsNotCompleted = "At least one of the predecessors is not yet completed";
  7.     public static readonly String PredecessorSelfReference = "A task cannot be its own predecessor";
  8.     public static readonly String DueDateEarlierThanStartDate = "Due date should not be earlier than start date ";
  9.     public static readonly String CompletedTaskPercentage = "For a completed task the % complete should be 100";
  10. }

Despite of the constant values used in the sample, it is easy to create dynamic messages as well. For example, you can define a message like "Task title ‘{0}’ is not unique", then substitute the current value when setting the validation error.

The first rule is to allow only unique task titles.

  1. TextField titleField = GetFieldControlByName(SampleTaskListFields.Title) as TextField;
  2.  
  3. if (titleField != null)
  4. {
  5.     String title = titleField.Value as String;
  6.  
  7.     if (!String.IsNullOrEmpty(title))
  8.     {
  9.         if (ItemWithSameTitleExists(title))
  10.         {
  11.             SetValidationError(SampleTaskListFields.Title, ValidationMessages.TitleNotUnique);
  12.         }
  13.     }
  14. }

We check the uniqueness through a CAML query, and use the GetFieldRefs helper method to build the value of the ViewFields property of the SPQuery instance.

  1. protected bool ItemWithSameTitleExists(string title)
  2. {
  3.  
  4.     SPQuery query = new SPQuery();
  5.  
  6.     query.ViewFields = GetFieldRefs(SampleTaskListFields.Title);
  7.     // we should not check the ID in this case, as altering the title is not allowed
  8.     // title will be validated only on new item creation
  9.     // if title would be editable for existing items, then we should check
  10.     // whether the ID is not the one of the current (edited) item
  11.     // note, that CAML Eq for a Text field type is case insensitive
  12.     query.Query = String.Format("<Where><Eq><FieldRef Name='{0}'/><Value Type='Text'>{1}</Value></Eq></Where>",
  13.         SampleTaskListFields.Title, title);
  14.  
  15.     bool result = List.GetItems(query).Count > 0;
  16.  
  17.     return result;
  18. }
  19.  
  20. private String GetFieldRefs(params String[] fieldNames)
  21. {
  22.     String fieldRefs = String.Concat(fieldNames.ToList().
  23.         ConvertAll(fieldName => String.Format("<FieldRef Name='{0}'/>", fieldName)));
  24.  
  25.     return fieldRefs;
  26. }

The next image shows the validator in action.

image

The comparison is case insensitive.

image

Yes, you are right it is nothing more than a custom implementation for the built-in Enforce unique values feature, but this one does not require indexed columns, and you have the chance to handle issues like leading / trailing spaces. For example, if you enable Enforce unique values, it won’t prohibit users entering ‘ Sample task 1  ‘ without any difficulties. If you don’t like this, you can trim the title parameter value and/or use Contains instead of Eq in the CAML query.

After this basic validation, let’s see a more tricky one. This one can be solved using standard field validation, although in this case it is more complicated “thanks” to the read-only Start date field when the task is not in the “Not Started” status.

  1. DateTimeField startDateField = GetFieldControlByName(SampleTaskListFields.StartDate) as DateTimeField;
  2. DateTimeField dueDateField = GetFieldControlByName(SampleTaskListFields.DueDate) as DateTimeField;
  3.  
  4. // if the task is not in 'Not Started' status, then we display the start date
  5. // as read-only and value would be null
  6. // in this case we should get the real value, stored in the item
  7. DateTime? startDateValue = (startDateField.ControlMode == SPControlMode.Display) ? (DateTime?)Item[SampleTaskListFields.StartDate] : startDateField.Value as DateTime?;
  8. DateTime? dueDateValue = dueDateField.Value as DateTime?;
  9.  
  10. if ((startDateValue.HasValue) && (dueDateValue.HasValue))
  11. {
  12.     if (dueDateValue.Value < startDateValue.Value)
  13.     {
  14.         SetValidationError(SampleTaskListFields.DueDate, ValidationMessages.DueDateEarlierThanStartDate);
  15.     }
  16. }

The illustration of the validation error:

image

This validation is again a simple one. We require 100% for % Complete when Status is Completed.

  1. DropDownChoiceField statusField = GetFieldControlByName(SampleTaskListFields.Status) as DropDownChoiceField;
  2. NumberField percComplField = GetFieldControlByName(SampleTaskListFields.PercentComplete) as NumberField;
  3.  
  4. bool checkForPredecessorStatus = false;
  5.  
  6. if ((statusField != null) && (percComplField != null))
  7. {
  8.     String statusFieldValue = statusField.Value as String;
  9.     Double? percComplValue = percComplField.Value as Double?;
  10.  
  11.     if (statusFieldValue == TaskStates.Completed)
  12.     {
  13.         checkForPredecessorStatus = true;
  14.         if ((!percComplValue.HasValue) || (percComplValue.Value != 1))
  15.         {
  16.             SetValidationError(SampleTaskListFields.PercentComplete, ValidationMessages.CompletedTaskPercentage);
  17.         }
  18.     }
  19. }

The screenshot of the validation:

image

Before you leave the post unread thinking this technique provides no additional value for validation, let’s see some more advanced example.

Using the standard Tasks list you can set an existing task as its own predecessor. That is not very nice, and in this case I found no simple OOB SharePoint tool to prohibit that.

Using this technique, you can achieve the result as shown below:

  1. MultipleLookupField predecessorsField = GetFieldControlByName(SampleTaskListFields.Predecessors) as MultipleLookupField;
  2.  
  3. if (predecessorsField != null)
  4. {
  5.     SPFieldLookupValueCollection predecessorsValue = predecessorsField.Value as SPFieldLookupValueCollection;
  6.  
  7.     if (predecessorsValue != null)
  8.     {
  9.         // a task can reference itself only in edit mode
  10.         if (ControlMode == SPControlMode.Edit)
  11.         {
  12.             if (!predecessorsValue.TrueForAll(predecessor => predecessor.LookupId != ItemId))
  13.             {
  14.                 SetValidationError(SampleTaskListFields.Predecessors, ValidationMessages.PredecessorSelfReference);
  15.             }
  16.         }
  17.         if (checkForPredecessorStatus)
  18.         {
  19.             if (!IsAllTasksCompleted(predecessorsValue.ConvertAll(predecessor => predecessor.LookupId)))
  20.             {
  21.                 SetValidationError(SampleTaskListFields.Status, ValidationMessages.PredecessorsNotCompleted);
  22.             }
  23.         }
  24.     }
  25. }

And in action:

image

The code above contains a check of predecessors when the task is to be set Completed. (The value of checkForPredecessorStatus computed earlier when validating % Complete.) We don’t want to allow that if there is at least on uncompleted predecessor. For the check we use the IsAllTasksCompleted method, passing the task ID of all of the predecessors as parameter.

  1. private bool IsAllTasksCompleted(List<int> taskIds)
  2. {
  3.     SPQuery query = new SPQuery();
  4.     query.ViewFields = GetFieldRefs(
  5.         SampleTaskListFields.Id,
  6.         SampleTaskListFields.Status);
  7.  
  8.     query.Query = String.Format("<Where><Neq><FieldRef Name='{0}'/><Value Type='Text'>{1}</Value></Neq></Where>",
  9.                     SampleTaskListFields.Status, TaskStates.Completed);
  10.  
  11.     bool result = true;
  12.     
  13.     SPListItemCollection tasksNotCompleted = List.GetItems(query);
  14.  
  15.     foreach (SPListItem task in tasksNotCompleted)
  16.     {
  17.         if (taskIds.Contains((int)task[SampleTaskListFields.Id]))
  18.         {
  19.             result = false;
  20.             break;
  21.         }
  22.     }
  23.  
  24.     return result;
  25. }

The result of the validation is shown here:

image

Next, we want to allow to assign a user to the task if both start and due dates are set.

  1. UserField userField = GetFieldControlByName(SampleTaskListFields.AssignedTo) as UserField;
  2.  
  3. if (userField != null)
  4. {
  5.     String userFieldValue = userField.Value as String;
  6.  
  7.     if (!String.IsNullOrEmpty(userFieldValue))
  8.     {
  9.  
  10.         if ((startDateValue.HasValue) && (dueDateValue.HasValue))
  11.         {
  12.             if (startDateValue.Value <= dueDateValue.Value)
  13.             {
  14.                 SPFieldUserValue userValue = new SPFieldUserValue(Web, userFieldValue);
  15.                 int? taskId = (base.ControlMode == SPControlMode.Edit) ? ItemId : (int?)null;
  16.                 if ((userValue.LookupId != -1) && (UserIsBusy(userValue.LookupId, taskId, startDateValue.Value, dueDateValue.Value)))
  17.                 {
  18.                     SetValidationError(SampleTaskListFields.AssignedTo, ValidationMessages.UserBusy);
  19.                 }
  20.             }
  21.         }
  22.         else
  23.         {
  24.             SetValidationError(SampleTaskListFields.AssignedTo, ValidationMessages.NoDateForAssignement);
  25.         }
  26.     }
  27. }

On the screenshot below, I “forgot” to set the start date, that is not allowed in this case.

image

Finally, using the method described in this post, we allow to assign the user for the task, if (s)he has no other assignment for that time interval. In this case we should ignore the task being edited when doing the validation.

  1. protected bool UserIsBusy(int userId, int? taskId, DateTime startDate, DateTime dueDate)
  2. {
  3.  
  4.     SPQuery query = new SPQuery();
  5.     query.ViewFields = GetFieldRefs(
  6.         SampleTaskListFields.Id,
  7.         SampleTaskListFields.AssignedTo,
  8.         SampleTaskListFields.StartDate,
  9.         SampleTaskListFields.DueDate);
  10.  
  11.     // NOTE: you can add filter for Status Neq 'Completed' as well if you wish
  12.     // I have not included that for the sake of simplicity
  13.     if (taskId.HasValue)
  14.     {
  15.         // it is editing an existing task, so we should exclude the task itself
  16.         query.Query = String.Format("<Where><And><And><Neq><FieldRef Name='{0}'/><Value Type='Integer'>{1}</Value></Neq><Eq><FieldRef Name='{2}' LookupId='TRUE' /><Value Type='Lookup'>{3}</Value></Eq></And>{4}</And></Where>",
  17.                         SampleTaskListFields.Id, taskId.Value,
  18.                         SampleTaskListFields.AssignedTo, userId,
  19.                         BuildDateRangeOverlapFilter(startDate, dueDate));
  20.     }
  21.     else
  22.     {
  23.         // it is a new task, we don't have to check te task ID
  24.         query.Query = String.Format("<Where><And><Eq><FieldRef Name='{0}' LookupId='TRUE' /><Value Type='Lookup'>{1}</Value></Eq>{2}</And></Where>",
  25.                         SampleTaskListFields.AssignedTo, userId,
  26.                         BuildDateRangeOverlapFilter(startDate, dueDate));
  27.     }
  28.  
  29.     bool result = List.GetItems(query).Count > 0;
  30.  
  31.     return result;
  32. }
  33.  
  34. protected String BuildDateRangeOverlapFilter(DateTime startDate, DateTime endDate)
  35. {
  36.  
  37.     StringBuilder sb = new StringBuilder();
  38.     sb.Append(String.Format("<And>{0}{1}</And>",
  39.         BuildSimpleDateFilter(SampleTaskListFields.StartDate, endDate, "Leq"),
  40.         BuildSimpleDateFilter(SampleTaskListFields.DueDate, startDate, "Geq")));
  41.  
  42.     return sb.ToString();
  43.  
  44. }
  45.  
  46. protected String BuildSimpleDateFilter(String dateFieldName, DateTime filterDate, String relation)
  47. {
  48.     String datePattern = "yyyy-MM-ddT00:00:00Z";
  49.  
  50.     return String.Format("<{0}><FieldRef Name='{1}'/><Value Type='DateTime'>{2}</Value></{0}>", relation, dateFieldName, filterDate.ToString(datePattern));
  51.  
  52. }

And that is the outcome of the validation in this case:

image

Note: Other validation possibilities for Person or Group field type are to check the number of users set (you can allow a single user or multiple ones using the standard tools, but you can not expect exactly three of them), or to check if the specified users are from a specific Active Directory group.

Although the technique illustrated in this post require coding and not so straightforward as specifying a simple formula on the UI, I hope you can use it  effectively if the customer requirements demand something more sophisticated.

You can download the sample application from here.

November 9, 2011

Setting form field visibility and editability through custom list field iterators

Filed under: Custom forms, Dynamic method, SP 2010 — Tags: , , — Peter Holpar @ 00:27

In a recent post I showed you the basic of SharePoint list form customizations via custom list filed iterators.

In this post I provide a sample that can serve as a base for your form customization needs. From the sample you will learn how to alter the default display mode of the form fields, for example, hide fields on the edit or display form, or display fields as read-only on edit form. As a bonus, I will show you how to inject your code into the saving mechanism of list items and override the default behavior.

Before writing our own code, let’s see how the default field rendering is working. The following code snippet is the CreateChildControls method of the Microsoft.SharePoint.WebControls.ListFieldIterator class.

  1. protected override void CreateChildControls()
  2. {
  3.     this.Controls.Clear();
  4.     if (this.ControlTemplate == null)
  5.     {
  6.         throw new ArgumentException("Could not find ListFieldIterator control template.");
  7.     }
  8.     for (int i = 0; i < base.Fields.Count; i++)
  9.     {
  10.         SPField field = base.Fields[i];
  11.         if (!this.IsFieldExcluded(field))
  12.         {
  13.             TemplateContainer child = new TemplateContainer();
  14.             this.Controls.Add(child);
  15.             child.ControlMode = base.ControlMode;
  16.             child.FieldName = field.InternalName;
  17.             this.ControlTemplate.InstantiateIn(child);
  18.         }
  19.     }
  20. }

As you can see, hiding a field is quite straightforward, you should simply skip the field similarly to the IsFieldExcluded condition. In fact, the IsFieldExcluded property is a useful way to hide fields, but the method shown in the post hopefully provides a more flexible solution.

Displaying the field as read-only on the edit form would be possible through setting the ControlMode property of the TemplateContainer instance in the overriden version of CreateChildControls method.

Unfortunately, both ControlMode and FieldName properties are internal members of the TemplateContainer class, so there is no simple way to work with them in our custom CreateChildControls method.

That is where Reflection or dynamic methods come into the picture. In this case I chose the second one, and created the ILUtils helper class:

  1. public delegate void GenericSetter(object target, object value);
  2. public delegate object GenericGetter(object target);
  3.  
  4. class ILUtils
  5. {
  6.     ///
  7.     /// Creates a dynamic setter for the property
  8.     ///
  9.     public static GenericSetter CreateSetMethod(Type targetType, String propName)
  10.     {
  11.  
  12.         GenericSetter result = null;
  13.  
  14.         PropertyInfo propertyInfo = targetType.GetProperty(propName,
  15.             BindingFlags.NonPublic | BindingFlags.Instance);
  16.  
  17.         if (propertyInfo != null)
  18.         {
  19.             MethodInfo setMethod = propertyInfo.GetSetMethod(true);
  20.             if (setMethod != null)
  21.             {
  22.  
  23.                 Type[] arguments = new Type[2];
  24.                 arguments[0] = arguments[1] = typeof(object);
  25.  
  26.                 DynamicMethod setter = new DynamicMethod(
  27.                   String.Concat("_Set", propertyInfo.Name, "_"),
  28.                   typeof(void), arguments, propertyInfo.DeclaringType);
  29.                 ILGenerator generator = setter.GetILGenerator();
  30.                 generator.Emit(OpCodes.Ldarg_0);
  31.                 generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
  32.                 generator.Emit(OpCodes.Ldarg_1);
  33.  
  34.                 if (propertyInfo.PropertyType.IsClass)
  35.                     generator.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
  36.                 else
  37.                     generator.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType);
  38.  
  39.                 generator.EmitCall(OpCodes.Callvirt, setMethod, null);
  40.                 generator.Emit(OpCodes.Ret);
  41.  
  42.                 result = (GenericSetter)setter.CreateDelegate(typeof(GenericSetter));
  43.             }
  44.         }
  45.         return result;
  46.     }
  47.  
  48.     ///
  49.     /// Creates a dynamic getter for the property
  50.     ///
  51.     public static GenericGetter CreateGetMethod(Type targetType, String propName)
  52.     {
  53.  
  54.         GenericGetter result = null;
  55.  
  56.         PropertyInfo propertyInfo = targetType.GetProperty(propName,
  57.             BindingFlags.NonPublic | BindingFlags.Instance);
  58.  
  59.         if (propertyInfo != null)
  60.         {
  61.             MethodInfo getMethod = propertyInfo.GetGetMethod(true);
  62.             if (getMethod != null)
  63.             {
  64.  
  65.                 Type[] arguments = new Type[1];
  66.                 arguments[0] = typeof(object);
  67.  
  68.                 DynamicMethod getter = new DynamicMethod(
  69.                   String.Concat("_Get", propertyInfo.Name, "_"),
  70.                   typeof(object), arguments, propertyInfo.DeclaringType);
  71.                 ILGenerator generator = getter.GetILGenerator();
  72.                 generator.DeclareLocal(typeof(object));
  73.                 generator.Emit(OpCodes.Ldarg_0);
  74.                 generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
  75.                 generator.EmitCall(OpCodes.Callvirt, getMethod, null);
  76.  
  77.                 if (!propertyInfo.PropertyType.IsClass)
  78.                     generator.Emit(OpCodes.Box, propertyInfo.PropertyType);
  79.  
  80.                 generator.Emit(OpCodes.Ret);
  81.  
  82.                 result = (GenericGetter)getter.CreateDelegate(typeof(GenericGetter));
  83.             }
  84.         }
  85.  
  86.         return result;
  87.  
  88.     }
  89.  
  90. }

Above idea and most of the code are borrowed from this great post.

We will store display rule exceptions using FieldDisplayRuleItem instances. FieldDisplayRule determines if we should hide the field or display it as read-only. FieldNames and ControlModes stores the field names and control modes the rule applies to. We will see this class in action soon.

  1. // helper class to store display exception rules
  2. // you can add your own properties to extend logic
  3. public class FieldDisplayRuleItem
  4. {
  5.     // list of field names the rule applies to
  6.     public List<String> FieldNames { get; set; }
  7.     // list of control modes the rule applies to
  8.     public List<SPControlMode> ControlModes { get; set; }
  9.     // the resulting display exeption rule
  10.     public FieldDisplayRule Rule { get; set; }
  11. }
  12.  
  13. public enum FieldDisplayRule
  14. {
  15.     Hidden,
  16.     Display
  17. }

Note: you can extend the FieldDisplayRuleItem class as your requirements dictate. For example if the items you work with have a Status field it is common to include this field in the rules. Of course, in this case you should alter the logic that selects the items to get the hidden / read-only fields as illustrated later.

Using the dynamic methods helper class it is easy to create a setter for the ControlMode and FieldName properties of the TemplateContainer class, that we can use in the overriden CreateChildControls method of our base class.

  1. public class BaseListFieldIterator : Microsoft.SharePoint.WebControls.ListFieldIterator
  2. {
  3.     // create dynamic setter methods that wrap the internal
  4.     // ControlMode and FieldName properties of the TemplateContainer class       
  5.     protected static GenericSetter set_TemplateContainer_ControlMode =
  6.             ILUtils.CreateSetMethod(typeof(TemplateContainer), "ControlMode");
  7.     protected static GenericSetter set_TemplateContainer_FieldName =
  8.         ILUtils.CreateSetMethod(typeof(TemplateContainer), "FieldName");
  9.  
  10.     protected List<FieldDisplayRuleItem> _dynamicRules = new List<FieldDisplayRuleItem>();
  11.  
  12.     // get references for the frequently used objects
  13.     protected SPSite _site = SPContext.Current.Site;
  14.     protected SPWeb _web = SPContext.Current.Web;
  15.     protected SPContext _context = SPContext.Current;
  16.     protected SPFormContext _formContext = SPContext.Current.FormContext;
  17.  
  18.     protected override void CreateChildControls()
  19.     {
  20.         this.Controls.Clear();
  21.         if (this.ControlTemplate == null)
  22.         {
  23.             throw new ArgumentException("Could not find ListFieldIterator control template.");
  24.         }
  25.  
  26.         for (int i = 0; i < base.Fields.Count; i++)
  27.         {
  28.             SPField field = base.Fields[i];
  29.             String fieldName = field.InternalName;
  30.  
  31.             // check if the current field is on the list of "hidden" fields in the current display mode
  32.             // or whether there is a "global" rule to hide fields
  33.             FieldDisplayRuleItem exception = _dynamicRules.FirstOrDefault(
  34.                 // empty (null) value means there is no restriction for the control mode
  35.                 e => ((e.ControlModes == null) || (e.ControlModes.Contains(ControlMode))) &&
  36.                 // empty (null) value means there is no restriction for the field name
  37.                 ((e.FieldNames == null) || (e.FieldNames.Contains(fieldName))) &&
  38.                 (e.Rule == FieldDisplayRule.Hidden));
  39.  
  40.             if ((!this.IsFieldExcluded(field)) && (exception == null))
  41.             {
  42.                 TemplateContainer child = new TemplateContainer();
  43.                 this.Controls.Add(child);
  44.                 SPControlMode controlMode = GetControlMode(fieldName);
  45.                 // use the dynamic setter to access internal properties
  46.                 set_TemplateContainer_ControlMode(child, controlMode);
  47.                 set_TemplateContainer_FieldName(child, fieldName);
  48.                 this.ControlTemplate.InstantiateIn(child);
  49.             }
  50.         }
  51.     }
  52.  
  53.     private SPControlMode GetControlMode(string fieldName)
  54.     {
  55.  
  56.         FieldDisplayRuleItem rule = _dynamicRules.FirstOrDefault(
  57.             e => ((e.ControlModes == null) || (e.ControlModes.Contains(ControlMode))) &&
  58.             ((e.FieldNames == null) || (e.FieldNames.Contains(fieldName))) &&
  59.             (e.Rule == FieldDisplayRule.Display));
  60.  
  61.         SPControlMode result = (rule == null) ? ControlMode : SPControlMode.Display;
  62.  
  63.         return result;
  64.     }
  65.  
  66. }

Instead of using string literals in my methods, I usually use static classes to store string constants like field or list names:

  1. public static class SampleTaskListFields
  2. {
  3.     public static readonly String Id = "ID";
  4.     public static readonly String Title = "Title";
  5.     public static readonly String PercentComplete = "PercentComplete";
  6.     public static readonly String AssignedTo = "AssignedTo";
  7.     public static readonly String Status = "Status";
  8.     public static readonly String Predecessors = "Predecessors";
  9.     public static readonly String StartDate = "StartDate";
  10.     public static readonly String DueDate = "DueDate";
  11. }
  12.  
  13. public static class Lists
  14. {
  15.     public static readonly String SampleList = "SampleTaskList";
  16. }
  17.  
  18.  
  19. public static class TaskStates
  20. {
  21.     // we only declare the state names used in the sample
  22.     public static readonly String NotStarted = "Not Started";
  23.     public static readonly String Completed = "Completed";
  24. }

If you prefer working with Guids then you may consider using the SPBuiltInFieldId class instead.

The exact implementation is derived from the BaseListFieldIterator class. We store static (global) rules in _staticRules and dynamic rules in _dynamicRules.

In the current example we are working with a task list and would like to achieve the following results:

Static rules:

  • The Title field can be edited only when the task is created.
  • Status and % Complete fields are hidden on task creation (we will set this values from our custom save handler method later).

Dynamic rules:

  • Predecessors field is only editable by site administrators.
  • Start Date is only editable if the item is in the Not Started status.

The following code creates the rules for these requirements.

  1. public class ListFieldIterator : BaseListFieldIterator
  2. {
  3.  
  4.     protected static List<FieldDisplayRuleItem> _staticRules = new List<FieldDisplayRuleItem>();
  5.  
  6.     static ListFieldIterator()
  7.     {
  8.  
  9.         // task title can be set on item creation
  10.         // but can't be altered later
  11.         _staticRules.Add(
  12.              new FieldDisplayRuleItem
  13.              {
  14.                  FieldNames = new List<String>() { SampleTaskListFields.Title },
  15.                  ControlModes = new List<SPControlMode>() { SPControlMode.Edit },
  16.                  Rule = FieldDisplayRule.Display
  17.              });
  18.  
  19.         // we don't allow to set status and % info on new item form
  20.         // items will be created with status "Not Started" and 0 %
  21.         // these values are set on item saving from code
  22.         _staticRules.Add(
  23.              new FieldDisplayRuleItem
  24.              {
  25.                  FieldNames = new List<String>() { SampleTaskListFields.Status, SampleTaskListFields.PercentComplete },
  26.                  ControlModes = new List<SPControlMode>() { SPControlMode.New },
  27.                  Rule = FieldDisplayRule.Hidden
  28.              });
  29.     }
  30.  
  31.  
  32.     protected override void OnInit(EventArgs e)
  33.     {
  34.         base.OnInit(e);
  35.  
  36.         // register save handler if not in display mode and form is posted back
  37.         if ((Page.IsPostBack) && (ControlMode != SPControlMode.Display))
  38.         {
  39.             _formContext.OnSaveHandler += new EventHandler(SaveHandler);
  40.         }
  41.  
  42.         CreateDynamicExceptions();
  43.  
  44.     }
  45.  
  46.     private void CreateDynamicExceptions()
  47.     {
  48.         _dynamicRules.AddRange(_staticRules);
  49.  
  50.         // only site admins are allowed to edit predecessor tasks
  51.         // others can see only the predecessors regardless of the control mode
  52.         if (!_web.CurrentUser.IsSiteAdmin)
  53.         {
  54.             _dynamicRules.Add(
  55.                 new FieldDisplayRuleItem
  56.                 {
  57.                     FieldNames = new List<String>() { SampleTaskListFields.Predecessors },
  58.                     Rule = FieldDisplayRule.Display
  59.                 });
  60.         }
  61.  
  62.         // start date can be set only if the task is not started
  63.         if ((String)Item[SampleTaskListFields.Status] != TaskStates.NotStarted)
  64.         {
  65.             _dynamicRules.Add(
  66.                 new FieldDisplayRuleItem
  67.                 {
  68.                     FieldNames = new List<String>() { SampleTaskListFields.StartDate },
  69.                     Rule = FieldDisplayRule.Display
  70.                 });
  71.         }
  72.  
  73.     }
  74.  
  75.     protected void SaveHandler(object sender, EventArgs e)
  76.     {
  77.         Page.Validate();
  78.  
  79.         if (Page.IsValid)
  80.         {
  81.             // do custom activities, send mail, create task,
  82.             // set permissions etc.
  83.  
  84.             // new tasks are created with status 'Not Started' and 0% complete
  85.             if (ControlMode == SPControlMode.New)
  86.             {
  87.                 Item[SampleTaskListFields.PercentComplete] = 0;
  88.                 Item[SampleTaskListFields.Status] = TaskStates.NotStarted;
  89.             }
  90.  
  91.             // we should save the item explicitly
  92.             Item.Update();
  93.         }
  94.     }
  95. }

In the OnInit method we register of custom save handler method. In the SaveHandler method we set the values for the Status and % Complete fields if the item is a new task. That is the method you can include your custom actions into, like sending mails or setting permissions on items.

To test the solution, we should first deploy the assembly as well as our user control. Then create a Tasks list called SampleTaskList, and register our custom ListFieldIterator to all of its forms as shown in my former post.

image

Logged on as a site admin, we create a task item.

image

Although the Status and % Complete fields are not visible on the form, the values of the fields are populated.

image

Next, we create another task item, and set the first one as a predecessor.

image

After saving the item, we open it again in edit mode. The Title field displayed as read-only.

image

We log in as a standard site member (non-admin), and open the item once again. The Predecessors field is also read-only in this case. We set the Status to In Progress.

image

After re-opening the item we see that the Start Date field became read-only.

image 

I hope this example helps you to create SharePoint forms that fit better to the common customer needs. In the following posts I’m planning to build on this sample with other interesting features.

You can download the sample application from here.

October 16, 2011

Disabling item deletion at the SharePoint user interface

Filed under: Custom forms, ECB menu, JavaScript, Ribbon, SP 2010 — Tags: , , , , — Peter Holpar @ 21:58

Recently I worked on more projects where it was a nice-to-have requirement to disable deletion of specific items from a list.

Well, if you are at least a bit familiar with SharePoint your first idea might be the same as mine, that is to create a list item event receiver to capture ItemDeleting event and cancel it for your specific items. You are right, that should do the job, however I find this approach a bit ugly, as the users still have the option on the user interface to press the Delete Item button on the ribbon or select the Delete Item menu from the Edit Control Block (ECB) just to get that user-unfriendly warning from the event receiver cancellation to their face.

Wouldn’t it be much better to disable the corresponding user interface elements for the specific list items to prohibit deletion? As a safeguard, you can still apply an event receiver, but that would not be the standard way to stop users deleting the items from the browser.

That sounds great, but how to achieve the goal?

First, let’s see where that deletion options are for the user in a standard SharePoint web UI. The individual item can be deleted from the display and edit form ribbon and using the ECB menu of the selected item in a list view. Additionally, there is a Delete Item button on the ribbon of the list view that is to delete selected items.

In fact, disabling deletion on the UI of the display and edit form ribbon is pretty easy. You just need to use the TrimById method of the SPRibbon class in your form component passing the ID of the Delete Item button as the parameter, that is Ribbon.ListForm.Display.Manage.DeleteItem for the display form and  Ribbon.ListForm.Edit.Actions.DeleteItem for the edit form. If you need more info about how to create and inject your custom solution into the standard SharePoint form, read my former post.

In the overridden OnPreRender method of my customized ListFieldIterator I simply check if Delete Item button should be disabled, and if it should, then call the TrimById method using the right control ID.

  1. protected override void OnPreRender(EventArgs e)
  2. {
  3.     SPFormContext formContext = SPContext.Current.FormContext;
  4.  
  5.     if ((formContext.FormMode != SPControlMode.New) && (!IsDeletionEnabled))
  6.     {
  7.         String deleteButton = (formContext.FormMode == SPControlMode.Display) ?
  8.             "Ribbon.ListForm.Display.Manage.DeleteItem" : "Ribbon.ListForm.Edit.Actions.DeleteItem";
  9.         SPRibbon.GetCurrent(this.Page).TrimById(deleteButton);
  10.     }
  11. }

The IsDeletionEnabled is a custom property where we check (for example, based on list or list item properties or user permissions) if we would like to allow deletion or not.

  1. protected bool IsDeletionEnabled
  2. {
  3.     get
  4.     {
  5.         bool result = true;
  6.  
  7.         // your custom logic comes here
  8.         if (((String)Item["Title"]) == "Do not delete")
  9.         {
  10.             result = false;
  11.         }
  12.  
  13.         return result;
  14.     }
  15. }

That’s about the display / edit forms, let’s see the list view form, harder part of our goal.

As I’ve mentioned above there are two UI items to disable in the list view form, one is on the ribbon and another one in the ECB menu.

A serious difference between the UI components on the display / edit forms and the ones on the list view item forms is that in the first case we can check the condition and disable controls on the server side (we know which item the form is displayed for), while in the second case, user can select any items (or none of them) on the client side, so we should act accordingly on the client side without any postback of the page.

Unfortunately, I’ve started with the ribbon part and it caused me some headache and long hours spent unsuccessfully to play with ribbon extensions to check conditions and interact with the existing controls using jQuery and other tricks as well.

As none of my solutions for the list view ribbon worked perfectly, I switched to ECB.

When you need to work with the ECB menu, this series of post from Jan Tielens provides an excellent starting point. Although it is written originally for WSS 3.0, the content is still valid for SharePoint 2010 as well.

Since my goal was not to add custom items to the menu, but rather to delete an existing one, the trivial solution was to create my custom version of AddListMenuItems. The original version of this JavaScript method (assuming English – United States locale ID 1033) can be found in the 14\TEMPLATE\LAYOUTS\1033\CORE.JS file, but to have a more readable one, get it from CORE.debug.js instead.

Note: There are tons of posts on the web about how to inject JavaScript code into your page. Most of them favor to add a Content Editor Web Part to the page and paste your code into that. However, I feel this approach to be more appropriate for a  power-user, and usually add my scripts through a ScriptLink CustomAction or via an AdditionalPageHead. I found these methods are easier to integrate into and deploy from a Visual Studio 2010 solution, but they have a drawback that they do not affect only the target page. If you need a targeted solution you should first verify  in the code of the AdditionalPageHead if it is the right list then register the script using Page.ClientScript.RegisterClientScriptBlock, or through one of the several version of the static Register* method of ScriptLink  class.

  1. SPContext context = SPContext.Current;
  2.  
  3. if ((context != null) && (context.List != null) && (context.List.Title == "YourList"))
  4. {
  5.     // register your script here
  6. }

To target the script in the case of a ScriptLink CustomAction you can apply the method described in this post.

Back to the topic, whatever solution you choose to reference and deploy the JavaScript code, you should alter the copy of the original version of the AddListMenuItems method as described below. 

Note: It is important not to tamper with the original version as it affects all of your SharePoint sites and what might be even worse, it is not a supported modification.

Look up the following code section in the AddListMenuItems method. It is responsible for creation of the Delete Item menu item in ECB.

  1. if (currentItemID.indexOf(".0.") < 0 && HasRights(0x0, 0x8)
  2.       && !currentItemIsEventsExcp)
  3. {
  4.     if (ctx.listBaseType==4)
  5.         strDisplayText=L_DeleteResponse_Text;
  6.     else
  7.         strDisplayText=L_DeleteItem_Text;
  8.     strAction="DeleteListItem()";
  9.     strImagePath=ctx.imagesPath+"delitem.gif";
  10.     menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1180);
  11.     CUIInfo(menuOption, "Delete", ["Delete"]);
  12.     menuOption.id="ID_DeleteItem";
  13.     CUIInfo(menuOption, "Delete", ["Delete"]);
  14. }

Let’s modify that code a bit as shown here:

  1. if (currentItemID.indexOf(".0.") < 0 && HasRights(0x0, 0x8)
  2.       && !currentItemIsEventsExcp) {
  3.     if (allowDeletion(currentItemID)) {
  4.         if (ctx.listBaseType == 4)
  5.             strDisplayText = L_DeleteResponse_Text;
  6.         else
  7.             strDisplayText = L_DeleteItem_Text;
  8.         strAction = "DeleteListItem()";
  9.         strImagePath = ctx.imagesPath + "delitem.gif";
  10.         menuOption = CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1180);
  11.         CUIInfo(menuOption, "Delete", ["Delete"]);
  12.         menuOption.id = "ID_DeleteItem";
  13.         CUIInfo(menuOption, "Delete", ["Delete"]);
  14.  
  15.     }
  16. }

Yes, the only difference is the extra condition that calls allowDeletion method passing the currentItemID parameter. The value of this variable is the ID of the actual list item.

How you implement allowDeletion in your JavaScript is up to your business requirements. Here are two examples of what can be achieved simply:

Disable deleting items having a specific ID. Probably not the most useful example but the simplest one for sure.

  1. function allowDeletion(itemID) {
  2.     return ((itemID == 1) || (itemID == 3));
  3. }

Disable deleting folders. Useful if you deployed a folder structure into your list and would like to prohibit users from destroying that.

  1. function allowDeletion(itemID) {
  2.  
  3.     var selectedItems = SP.ListOperation.Selection.getSelectedItems();
  4.     var result = false;
  5.  
  6.     for (var i = 0; i < selectedItems.length; i++) {
  7.  
  8.         if (selectedItems[i].id == itemID) {
  9.             // true (enable deletion), if it is a list item, not a folder
  10.             result = (selectedItems[i].fsObjType == 0);
  11.             break;
  12.         }
  13.     }
  14.  
  15.     return result;
  16.  
  17. }

If you need more sophisticated logic, like doing comparison with list item field values, you should apply some trick using WCF Data Services or even hacks to utilize the client object model but more about that in a forthcoming post.

And the most interesting part (at least, for me) of the story is coming now.

After deploying my script and testing the solution I realized that if you select an item that has its Delete Item menu removed from the ECB menu then Delete Item button on the ribbon is disabled. Nice! That is what I worked a lot earlier for without success. And that is the best that if you select multiple items from the list, and deletion is disabled at least for one of them the corresponding ribbon button is disabled as well.

To understand what happens behind the curtains I added some alert statements to my scripts (the simplest way of JavaScript debugging). It turned out, that the AddListMenuItems method (and our allowDeletion method as well) called once for each list item when they got selected first time. It means if you select multiple items by checking the topmost checkbox in the header, the methods are triggered for each items one by one. If a selected item is deselected, and selected the second (or even more) times, the methods are not triggered once more, so the state of the Delete Item menu won’t change. If it was removed (not added to be exact) the first time, it remains removed. If it was added the first time, it remain there. Of course, a page reload resets this kind of caching.

I removed (commented out) the script lines from this part of the function one by one, and found that the menu item actually gets added by this line:

menuOption = CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1180);

However, if I remove this method call (actually, there are two of the same line there):

CUIInfo(menuOption, "Delete", ["Delete"]);

then the Delete Item ribbon button won’t be enabled, even if there are items to delete. In this case, the items can be still deleted through ECB’s Delete Item menu.

If you check the “signiture” of this method it is:

function CUIInfo(menuItem, command, enabledCommands)

That means it registers the “Delete” command for our menu item and sets the “Delete” command as an enabled command as well. I have not tracked the code any further, but it seems that the Delete Item button on the ribbon is enabled only if all of the selected list items have a menu item with enabled commands (CUIEnabledCommands property) containing the “Delete” command. Seems to be a very interesting infrastructure behind this.

Note: The method described here does not protect you from items being deleted in the Datasheet View or other customized views, not to mention deleting items from custom code or web service / WCF calls (like client object model WCF Data Services etc.).

December 25, 2009

Interacting with other fields from a custom field

Filed under: Custom fields, SharePoint — Tags: , — Peter Holpar @ 19:28

In my recent post I wrote about hiding custom fields just to interact with other fields on the form. Today I will show you how you can create the interaction itself between the fields.

It requires some little tricks, but after we solve the exercise you will see that it is not so complicate and in the forthcoming post I will show you some very powerful example to demonstrate you the usability of this technique.

The main point of the interaction is to get the reference to the other field controls. As I did last time, I will first introduce an interesting approach that – sad but true – does not work.

On the forms the fields are rendered within a ListFieldIterator control. Individual fields of type SPField can be addressed through the Fields collection (inherited from FormComponent) of this class that returns an SPFieldCollection. SPField class has a property called FieldRenderingControl that returns BaseFieldControl. Since the actual ListFieldIterator can be accessed through the parent chain of the actual field control, at the first sight it seems to be a good idea to get the reference for another field using the following really long line of code:

BaseFieldControl fieldControl = ((Microsoft.SharePoint.WebControls.ListFieldIterator)this.Parent.Parent.Parent.Parent.Parent).Fields[fieldName].FieldRenderingControl;

Unfortunately that is not so simple. Although we got a valid reference to a field control, if you try to manipulate its properties, or just to read them, it turns out to be an other instance, not the one displayed on the forms. That is because the FieldRenderingControl returns a new instance of a subclass of the BaseFieldControl, that has really no relation to the actual form.

What other ways there are to get the reference field control? We know that it is a control on the page, but we don’t want to iterate through all of the controls just to find the field. Fortunately the BaseFieldControl class implements a specific interface IValidator and thus included in the Validators collection of the Page class. That is really nice, and we will depend on this behavior in a later post when implementing custom runtime validation of fields.

But for now it is enough to get the reference to the field control using a custom method like this:

  1. protected BaseFieldControl GetFieldControlByName(String fieldNameToSearch)
  2. {
  3.     String iteratorId = GetIteratorByFieldControl(this).ClientID;
  4.     foreach (IValidator validator in Page.Validators)
  5.     {
  6.         if (validator is BaseFieldControl)
  7.         {
  8.             BaseFieldControl baseField = (BaseFieldControl)validator;
  9.             String fieldName = baseField.FieldName;
  10.             if ((fieldName == fieldNameToSearch) &&
  11.                 (GetIteratorByFieldControl(baseField).ClientID == iteratorId))
  12.             {
  13.                 return baseField;
  14.             }
  15.         }
  16.     }
  17.     return null;
  18. }
  19.  
  20. private ListFieldIterator GetIteratorByFieldControl(BaseFieldControl fieldControl)
  21. {
  22.     return (Microsoft.SharePoint.WebControls.ListFieldIterator)this.Parent.Parent.Parent.Parent.Parent;
  23. }

In this method we iterate through the validators on the page, and do some checks in the following order:

  1. Is the field derived from the BaseFieldControl class?
  2. Is the field name the same we are looking for?
  3. An extra check just to be sure: is the field rendered in the same ListFieldIterator as the current field control. Without this check it is possible to have multiple forms on the same page having fields with the same name. We really don’t want to interact with fields in other forms. At least, not now…

Let’s see some simple practical example for usage of this method. Of course, in a production environment you should add some extra logic for null values, exception handling and tracing/logging, but I try to keep it simple now. All of the code snippets are for the field control class that is derived from the BaseFieldControl class.

In the first example we set the field control mode to display. That can you use to prevent users to modify the field value, for example based on the user’s permissions or the value of other field.

  1. protected void SetFieldReadOnly(String fieldName)
  2. {
  3.     BaseFieldControl fieldControl = GetFieldControlByName(fieldName);
  4.     fieldControl.ControlMode = SPControlMode.Display;           
  5. }

Sometimes it is required to hide the field value too. The next example hides the field having the specified name using the same technique we used to inject invisible field to the form.

  1. protected void HideField(String fieldName)
  2. {
  3.     BaseFieldControl fieldControl = GetFieldControlByName(fieldName);
  4.     fieldControl.Visible = false;
  5.     fieldControl.Parent.Parent.Visible = false;
  6. }

You can also set values of other fields using the same technique as illustrated in the code snippet below for a text field.

  1. protected void SetFieldValue(String fieldName, String fieldValue)
  2. {
  3.     TextField fieldText = (TextField)GetFieldControlByName(fieldName);
  4.     fieldText.Value = fieldValue;
  5.     fieldText.Text = fieldValue;
  6. }

You might ask why I don’t show you the simplest case: reading field value. That is because it is really not so simple, and I will describe it in details in a forthcoming post.

It might be useful to use these and similar methods in your custom field control and use that as the boilerplate class for more advanced fields.

The next code snippet assumes that you have a custom list with fields Field1, Field2, Field3, Field4 (all of these of type Single line of text), the default Title field and Field5 of type Choice with values Value1, Value2 and Value3. Furthermore, I suggest you to create your custom field as a “hidden” field as described here.

I’ve created an item in the list before adding our new field to the list columns. The following figure shows the values of the fields on the edit form. You can see there is nothing special on that:

image

We call the methods in the edit and new control modes of the field from the OnLoad method of the field control:

  1. if ((ControlMode == SPControlMode.Edit) || (ControlMode == SPControlMode.New))
  2. {
  3.     HideField("Field1");
  4.     SetFieldReadOnly("Field3");
  5.     SetFieldReadOnly("Field5");
  6.     SetFieldValue("Field4", "text");
  7. }

After building, deploying, and adding the new field to the list, you should see the results immediately on the edit form:

image

It seems to be working perfectly, but if you would like to create you will notice that there is a minor issue with the code. Field3 should be empty, but it contains the text Field3 field value. The other read only field, Field5 of type Choice is displayed correctly.

image

See the second part of this post for the solution of the issue.

Next time I try to plan to show you something more exciting using the same technique.

September 16, 2009

Workaround for the custom site column property bug

Filed under: Bugs, Custom fields, SharePoint — Tags: , , — Peter Holpar @ 03:35

As so many other SharePoint user, administrator or developer (see MSDN forum threads here: Custom Field Type error, but only for site columns, Custom Field Selection in Site Column fails), we also found that after the SP2 our custom fields that have the PropertySchema tag and custom properties defined in their XML definition file, face to the error below:

Object reference not set to an instance of an object. at 
Microsoft.SharePoint.WebControls.FieldProperty.Render(HtmlTextWriter output)

As we already had issues with this kind of custom field properties (described well here: SharePoint Custom Field Weirdness), we assumed that the source of the problems is the PropertyIterator that is responsible for the custom field property rendering

The code behind for the FldNewEx.aspx page is in the FieldNewPage class, and the code behind for the FldEditEx.aspx page is the FieldEditPage class, both of these are derived from the FieldCustomizationPage class. Each of these classes are in the Microsoft.SharePoint.ApplicationPages namespace and in the Microsoft.SharePoint.ApplicationPages assembly. The CreateChildControls() method of the FieldCustomizationPage class creates a ListFieldIterator instance based on the PropertyIterator template, and loads that into the Customization control of PlaceHolder type.

The PropertyIterator template is defined in the DefaultTemplates.ascx file in the TEMPLATE\CONTROLTEMPLATES folder. After a short experimenting and a few IISRESET it was clear to us, that the error can be corrected, when we remove the line stroked through below (see the remark at the bottom of the post!), of course, only after creating a backup copy of the DefaultTemplates.ascx into another (it is important not into the CONTROLTEMPLATES) folder, for example C:\Temp is a perfect choice:

<SharePoint:RenderingTemplate ID="PropertyIterator" 
runat="server">
<Template>
<TR>
<TD nowrap="true"
valign="top" class="ms-authoringcontrols">
<SharePoint:FieldLabel
runat="server"/>
</TD>
<TD valign="top"
class="ms-authoringcontrols">
<SharePoint:FormField
runat="server"/>
<SharePoint:FieldDescription
runat="server"/>
</TD>
</TR>
</Template>
</SharePoint:RenderingTemplate>

After this modification and an IISRESET the pages that did not work, loaded successfully. But the labels for the property names were missing as shown below:

clip_image002_thumb

Of course it is not the best, as user would like to know which property do they modify.

Based on the PropertyIterator template we assumed, that there must be a description field too (see the FieldDescription control in the template above).We have not used that kind of field for the custom properties, and I think it is a not well-documented one, but we tried it and it worked:

<PropertySchema>
<Fields>
<Field Name="ErrorMessage"
DisplayName="Error Message" MaxLength="255" DisplaySize="25" Type="Text"
Description="Error Message">
</Field>
<Field
Name="InvalidLengthMessage" DisplayName="Invalid length message" MaxLength="255"
DisplaySize="25" Type="Text" Description="Invalid Length
Message"
>
</Field>
</Fields>
</PropertySchema>

If we set the Description attribute value to be the same as the DisplayName attribute, then the result is almost the same, as we hoped it to be:

clip_image004_thumb

If we go one step further, and rearrange the controls in the PropertyIterator template like this:

<SharePoint:RenderingTemplate ID="PropertyIterator" 
runat="server">
<Template>
<TR>
<TD nowrap="true"
valign="top" class="ms-authoringcontrols">
<SharePoint:FieldDescription
runat="server"/>
</TD>
<TD valign="top"
class="ms-authoringcontrols">
<SharePoint:FormField
runat="server"/>
</TD>
</TR>
</Template>
</SharePoint:RenderingTemplate>

then the description label will appear in the place of the property name, and the result is about matching to the one, that the original version should be:

clip_image006_thumb

Don’t forget to run IISRESET after modifications in the DefaultTemplates.ascx or in the field definition XML file.

Important remark: Altering DefaultTemplates.ascx migth not be a supported action, so it is better to create a separate file in the TEMPLATE\CONTROLTEMPLATES folder, for example call it PropertyIterator.ascx, and copy the headers and the PropertyIterator template from the DefaultTemplates.ascx, and make the modifications on this template. Altered version in the separate file will override the template in the DefaultTemplates.ascx.

UPDATE: 03.12.2009: For those of you who have not heard about that, there is a hotfix for this issue that is included in the "Windows SharePoint Services 3.0 Cumulative Update Server Hotfix Package (Sts.msp): August 25, 2009". Thanks to Geert van Raaij on the MSDN threads referenced above for sharing the link. There were rumors we heard from MS guys at the beginning of October that the bug is fixed in the latest cumulative patch, but they did not share the information where one can get that from.

Blog at WordPress.com.