A Quick Note About DynamicValue Objects in Workflows

I'm still, for some reason, flummoxed by how to effectively debug a DynamicValue object. In SharePoint 2013, it's no longer possible to write server-side code directly in a workflow class (as we all know by now). These days, you'll have to call one of the SharePoint CSOM REST services or something like that using the HttpSend workflow activity to perform many of the operations that you used to do in a code activity in SharePoint 2007 or 2010. There are plenty of tutorials available to teach you how to set up parameters, but it frequently doesn't go that easy. So what do you do when things just aren't working?

Well, I know what you can't do, because it will cause the workflow to suspend and terminate.

What you don't do, is call DynamicValue.ToString() in a WriteToHistory activity. I desperately wanted to see what the JSON return value looked like in the workflow, or even the response headers, and I really hoped that it would deliver something to my history log that I could work with. But no, the workflow died, went to a suspended state, and eventually dropped this under the "information" icon on the workflow status page:

RequestorId: dcfe3e96-d4ab-be6a-0000-000000000000. 
Details: RequestorId: dcfe3e96-d4ab-be6a-0000-000000000000. 
Details: An unhandled exception occurred during the execution of the workflow instance. 
Exception details: System.ApplicationException: HTTP 500
{
    "Transfer-Encoding": ["chunked"],
    "X-SharePointHealthScore": ["0"],
    "SPClientServiceRequestDuration": ["20"],
    "SPRequestGuid": ["dcfe3e96-d4ab-be6a-8784-0176cb71f2cd"],
    "request-id": ["dcfe3e96-d4ab-be6a-8784-0176cb71f2cd"],
    "X-FRAME-OPTIONS": ["SAMEORIGIN"],
    "MicrosoftSharePointTeamServices": ["15.0.0.4420"],
    "X-Content-Type-Options": ["nosniff"],
    "X-MS-InvokeApp": ["1; RequireReadOnly"],
    "Cache-Control": ["max-age=0, private"],
    "Date": ["Sat, 20 Dec 2014 12:46:02 GMT"],
    "Server": ["Microsoft-IIS\/8.0"],
    "X-AspNet-Version": ["4.0.30319"],
    "X-Powered-By": ["ASP.NET"]
}
at Microsoft.Activities.Hosting.Runtime.Subroutine.SubroutineChild.Execute(CodeActivityContext context) 
at System.Activities.CodeActivity.InternalExecute(ActivityInstance instance, ActivityExecutor executor, BookmarkManager bookmarkManager) 
at System.Activities.Runtime.ActivityExecutor.ExecuteActivityWorkItem.ExecuteBody(ActivityExecutor executor, BookmarkManager bookmarkManager, Location resultLocation) 

Which isn't the most helpful information. Doesn't tell you which activity, what the actual error is, anything helpful. Anyway, you'll probably see an error like this from time to time, and your error cause might not be the same as mine, but if you're messing with REST service calls and you want to see the results, go fire up Fiddler and find the request to look at the response. Don't try to call ToString() on the DynamicValue that comes back.

Renaming a Document Item in a SharePoint Document Library

tl;dr: item[SPBuiltInFieldId.FileLeafRef] = "filename";

So I'm back doing nuts and bolts type SharePoint development and last night, I couldn't remember quite how to rename a file in a document library. Sure, you can set item["Title"] to something new, but that doesn't change the file name (right?). Now, there are plenty of articles and Stack Overflow/Stack Exchange questions where people tell you to use the "Name" field to update the file name. Sure, that's cool. And it works. But, and hopefully you know about this already, for any of the built in fields in SharePoint, you should be using members from the SPBuiltInFieldId class whenever you access or update one of those fields.

So, stop writing,

item["Title"] = "A People's History of the United States";

or

item["Author"] = "dev\\hunter";

Instead of those magic strings, you should be using something like

item[SPBuiltInFieldId.Title] = "The Omnivore's Dilemma";

or

item[SPBuiltInFieldId.Author] = "dev\\not.hunter";

Or, even better, on that second one, get yourself an SPUser object like you're supposed to, create an SPFieldUserValue object using the user ID and login name. But that's a post for another day.

Back to how to rename a file in a document library, though. Instead of using item["Name"], please use:

item[SPBuiltInFieldId.FileLeafRef] = "Whatever ole filename you like.awesome";

Yes, "Name" works fine, but get in the habit of using the class you've been given. You'll save yourself some headaches at some point.

Using hMailServer for SharePoint Development

Exchange isn't the easiest thing in the world to set up, especially if you just want to set up user alerts and let lists receive incoming emails. hMailServer is a good substitute for your development server. Here's a pretty good guide for setting it up in your development environment. I recently set it up in my new SharePoint 2013 environment, and having done it three or four other times, I went quickly and forgot a couple of things about the process.

When I sent in emails to an email enabled document library, hMailServer returned the following error:

 

Your message did not reach some or all of the intended recipients.

Subject: test
Sent: 8/14/2013 8:27 PM
The following recipient(s) cannot be reached:
'somelist@thisdevserver.loc' on 8/14/2013 8:27 PM

Server error: '550 Mail server configuration error. Too many recursive forwards.'

What I forgot:  

Any time a list is set up for incoming email, the configuration in hMailServer has to be set up with the address as well.  Normally, when a list is configured for incoming email, SharePoint will set up Exchange to know about the address and how to deliver the message. If you're not using Exchange, SharePoint doesn't know how to set up the address. So you have to do it yourself.

Note that the guide linked above mentions that the catch-all address should make it so you don't have to do this. But I still do in my environment. If you happen to know why, leave a comment below so I can fix it. 

Go into hMailServer Administrator, and go to Domains -> <your domain> -> Accounts. Add an account with the address you use in the SharePoint list's incoming email address settings page, give it a password, and that's it. I don't know if any of the other options would cause it to fail, but feel free to experiment on your own to find out.

 

Hiding (Or Showing) Custom Actions

Custom actions can take several different forms the official list, and you will eventually need to add a custom action for only site administrators or site collection administrators. As you might suspect, the XML schema for defining custom actions absolutely supports both of those cases.

The Custom Action specification has RequireSiteAdministrator and Rights attributes that you can use to tailor access to the custom action. Note that this shouldn't be your only layer of security. If you're linking to a custom application page, you should also check the identity of the logged in user in the custom page, in case someone knows the URL to your page.

Limiting to Site Collection Administrators

Use the RequireSiteAdministrator field. It's a boolean, so you'll use

RequireSiteAdministrator="true"

or

RequireSiteAdministrator="false"

"Site Administrator", eh? Is that site level or SPSite level? Spoiler: SPSite level (see section heading). If you specify RequireSiteAdministrator=true, only users who are site collection administrators will see the link.

Limiting to Site (i.e., SPWeb) Administrators

Use

Rights="ManageWeb"
RequireSiteAdministrator="false"

to restrict the custom action to users who are site administrators. There are tons of options that you can specify in the Rights attribute, so if you're looking for something more nuanced than "site administrator or not", there might be an option waiting for you.

Unlimited Access

As you might expect, if you want to let any any user see the link, just leave "ManageWeb" out of the rights attribute, and specify

RequireSiteAdministrator="false"

A Conclusion (Of Sorts)

In your project in Visual Studio, ad a new item of the type "Empty Element". In the element file, add a CustomAction node and specify the combination of attributes you need. Double check to make sure that the element file is included in one of the features (and double check to make sure that the feature is included in the package, 'cause you never know).

As I mentioned in the overview, the only thing controlled by specifying these attributes is whether the custom action link is displayed to the user. It doesn't do anything to secure the page itself, so if users know the URL of your page, they can just browse directly to it. So, check the user in your page as well.

Selecting a few things from a bunch of things: Using the SharePoint GroupedItemPicker control

SharePoint has a bunch of neat controls that are used in the built in application pages, and you can use them yourself if you know how they work. For example, selecting content types to attach to a list uses the following control:

This is a GroupedItemPicker control, which you can find in the Microsoft.SharePoint.WebControls namespace, in Microsoft.SharePoint.dll. It uses a select control for groups, two select boxes for items, two buttons to move items from the left side (candidate) to the right side (selected), and a span to show the description of any selected item. There's almost no good documentation on how to practically use this control, so let's take a look at how it's used here.

Markup

The application page that controls attaching content types to lists is at _layouts/AddContentTypeToList.aspx. You can examine the markup of the page just by looking at the file in the SharePoint hive under the Template/Layouts folder, i.e., c:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\AddContentTypeToList.aspx. The interesting bits look like the following:

<SharePoint:GroupedItemPicker id="Picker" runat="server" GroupControlId="SelectGroup" CandidateControlId="SelectCandidate" ResultControlId="SelectResult" AddButtonId="AddButton" RemoveButtonId="RemoveButton" DescriptionControlId="DescriptionControl" />
<table width="500px">
<tr>
<td class="ms-authoringcontrols" style="padding-right: 10px" colspan="3">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,addcontenttypetolist_select_section_text_select_from%>" EncodeMethod='HtmlEncode'/>
<br/>
<select id="SelectGroup" runat="server" title="<%$Resources:wss,ctypedit_select_group%>">
</select>
</td>
</tr>
<tr>
<td class="ms-authoringcontrols" valign="bottom" style="padding-right: 10px">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,addcontenttypetolist_select_section_text_available_templates%>" EncodeMethod='HtmlEncode'/>
</td>
<td>&#160;</td>
<td class="ms-authoringcontrols" valign="bottom" style="padding-right: 10px">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,addcontenttypetolist_select_section_text_content_types_on_list%>" EncodeMethod='HtmlEncode'/>
</td>
</tr>
<tr>
<td style="padding-right: 10px">
<SharePoint:SPHtmlSelect id="SelectCandidate" runat="server" multiple="true" title="<%$Resources:wss,fldpick_possible_flds%>"/>
</td>
<td align="center" valign="middle" class="ms-authoringcontrols" style="padding-right: 10px">
<button class="ms-ButtonHeightWidth " ID="AddButton" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,multipages_gip_add%>" EncodeMethod='HtmlEncode'/>
</button>
<br/>
<br/>
<button class="ms-ButtonHeightWidth " ID="RemoveButton" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,multipages_gip_remove%>" EncodeMethod='HtmlEncode'/>
</button>
</td>
<td class="ms-authoringcontrols" style="padding-right: 10px">
<SharePoint:SPHtmlSelect id="SelectResult" runat="server" multiple="true" title="<%$Resources:wss,fldpick_selected_flds%>"/>
</td>
</tr>
<tr>
<td class="ms-authoringcontrols" colspan="3">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,multipages_description%>" EncodeMethod='HtmlEncode'/><br/>
<span id="DescriptionControl" runat="server"> </span>&#160;
</td>
</tr>
</table>

One of the interesting things about it is that the control doesn't contain any complicated templates within the markup defining it. If you'll recall the implementation for a GridView control, for example, you'd put a bunch of tags inside the <asp:GridView> tag to define how to show rows and columns. No so, with the GroupedItemPicker control.

In this case, you'll give the control the ID of each of the elements that compose the functionality: GroupControlId, CandidateControlId, ResultControlId, AddButtonId, RemoveButtonId, DescriptionControlId. Then, anywhere else on the page (but most likely right after, as in this case), you'll add separate controls with the same IDs as in the parameters to the GroupedItemPicker control. You can see this in the sample above, which renders the controls in a standard table. Note a few things:

  1. The three selects are implemented differently in the markup: The group picker is a regular old <select> element, but the candidate list and selected lists are SPHtmlSelect elements. These are eventually rendered pretty much the same.
  2. There's no wiring up of the actions for the add and remove buttons. The under-the-covers action of the GroupedItemPicker control handles all of that. It's going to happen in JavaScript anyway, so there's no postback or AJAX wizardry to handle moving things back and forth.
  3. Because of the disconnected nature of the GroupedItemPicker control definition and the actual markup for the underlying controls, you can render this however you want! Go nuts branding a couple of awesome squiggly line images instead of just relying on plain looking buttons for add and remove actions, or rearrange the controls in a vertical orientation if you feel like it.
  4. The control does not define its own OK, Cancel, Revert, or any other buttons besides the two that move items between candidate and selected sides. You get to/have to do this yourself. In this case, the markup contains

    <asp:Button UseSubmitBehavior="false" runat="server" class="ms-ButtonHeightWidth"
    OnClick="Update" Text="<%$Resources:wss,multipages_okbutton_text%>" id="btnOK"
    accesskey="<%$Resources:wss,okbutton_accesskey%>"/>

    to handle that. This is your standard postback button definition that calls the Update method when clicked.

Initial Data Loading

OK, that's all fine and good, but how on earth do you get real data into and out of this thing? Here goes...

The code behind for this page is located in the Microsoft.SharePoint.ApplicationPages assembly, which is located in the ISAPI folder under the SharePoint hive. Open that up in .NET Reflector to see what the actual C# looks like. Or see below:

Some of this code relates to how content types work, so we'll skip that in favor of looking at how the SPGroupItemPicker code works. In the OnLoad method on this page, a couple of things are done to set up the page:

  1. Get references to data sources.
    SPContentTypeCollection availableContentTypes = base.Web.AvailableContentTypes;

  2. Call the AddItem method on the picker control with the id, name, description, and group.
    this.Picker.AddItem(type.Id.ToString(), name, ContentTypePageUtil.GetPickerAllGroupDescription(type), str2);
    this.Picker.AddItem(type.Id.ToString(), name, ContentTypePageUtil.GetPickerAllGroupDescription(type), group);
    
    • The id is a unique identifier that will be used by the control (behind the scenes, in JavaScript) to grab individual items and move them back and forth between candidate and selected lists. In this case, it's literally the content type ID from the system, e.g., 0x0108 for Task, 0x01 for Item.
    • The name is what will appear in the list for users to click.
    • The description is the text will appear in the description span when an item is clicked. In this case, the GetPickerAllGroupDescription method looks up the description from resource files.
    • The group is the entry in the groups dropdown that will display the item. Note that you don't have to define groups separately. The control will examine the collection of items and construct its own groups.

  3. The AddContentTypeToList page doesn't preselect content types that are already attached to the list, so if you have a list that already has a couple of content types attached, they don't appear in the right side of the control. There is a method called AddSelectedItem that will handle this for you if you need to show some items as preselected.

  4. The AddContentTypeToList page has a group containing all available content types in addition to the categorized groups. You can see how this is constructed by the two consecutive AddItem method calls. The first call adds the content type to the specific group, and the second adds the content type to the "All Content Types" group. Line 7, beginning
    string group = ...
    sets this up, initially, by loading the name of the "All Content Types" group for the active locale.

Handling Changes

Great, so now you have a page that renders items on the candidate side, categorizes them into groups, shows item descriptions when clicked, and supports moving them back and forth between the candidate and selected sides. Now you want to be able to do something meaningful with the selected items when the user clicks the OK button. Here's what the Update method looks like:

  1. Right off the bat, you can see how to get the items selected by the user.
    ICollection selectedIds = this.Picker.SelectedIds;
    This is a collection of strings containing the IDs of the selected items. Remember that the IDs are the same as you used when adding items in the previous section with the AddItem or AddSelectedItem method.

  2. Based on the IDs in the selected list, actual objects are looked up again and added to a List object.
    List<SPContentType> list = new List<SPContentType>();
    SPContentTypeCollection contentTypes = this.CurrentList.ContentTypes;
    SPContentTypeCollection availableContentTypes = base.Web.AvailableContentTypes;
    foreach (string str in selectedIds)
    {
            SPContentTypeId id = new SPContentTypeId(str);
            if ((contentTypes[id] == null) && (availableContentTypes[id] != null))
            {
                list.Add(availableContentTypes[id]);
            }
    }

  3. Glossing over some of the logic about content type order and whether they're allowed on the list, look at the code toward the bottom of the method
    List<SPContentType> list4 = new List<SPContentType>(uniqueContentTypeOrder);
    foreach (SPContentType type2 in list2)
    {
            if (type2.IsAllowedInContentTypeOrder)
            {
                list4.Add(type2);
            }
    }
    The list4 object contains the items that eventually will be attached or remain attached to the list.

  4. Attaching the content types
    this.CurrentList.RootFolder.UniqueContentTypeOrder = list4;
    this.CurrentList.RootFolder.Update();
    Finally, the content types are actually attached to the list.

Extending the Functionality

As I mentioned previously, the AddContentTypeToList page doesn't preselect the existing content types attached to the list, but I have implemented preselected items before. It's pretty easy to check whether an item is already attached in your data source and call AddSelectedItem instead of AddItem when you're setting things up. It does get a little tricky once the user makes some changes and clicks the OK button.

You have a couple of options for how to process the changes:

  1. Remove all attached items and then add each selected item, both new and remaining, to the attached items. You can do this if you don't care about preserving internal IDs of the item attachments, and you don't care about the order of the attachments.

  2. Make changes to the existing attachments. This is a tricky one, algorithmically. My algorithm is something like: 1. get a list of existing attachments, 2. delete the ones that don't appear in the new selection list, 3. add the items in the new selection list, but only the ones that didn't appear in the selected list before.

So, like I said, it's a little tricky.

Wrapping it Up

This control could be considered a little bit superfluous. You could implement similar functionality with a list of checkboxes and just let the user check the ones that should be added. But that's no fun! And you'd have to roll your own description field. And you'd have to handle your own groups. When you look at it, this isn't really much code, and you get most of the user interface niceties for free from the JavaScript the control contains.

Think about some other ways this this control could be used. Enroll students in a few of the courses your organization offers. Attach tags or categories to blog posts. Assign individuals from your work group to various task steps in a larger process. Any time you want to assign or attach a subset of a group to an item, this control is a good candidate to get the job done.

Resources

SharePoint Saturday Philly

We went down to Philadelphia for SharePoint Saturday! Well, Malvern, PA, technically, but still. There was a great turnout, from attendees, as well as sponsors and speakers. Slalom had a great presence, with four speakers out of the 50 or so sessions (myself, Anil Ferris, Robert Hiskey, and Vijay Rajagopalan).

The day started super early, with us meeting at Grand Central at 6 a.m. to pick up the rental car. After a few wrong turns, and a really productive nap, we got to the Microsoft Technology Center in Malvern at about 9:00. We missed the keynote and opening remarks, but, c'est la vie, eh?

I presented in the 10 o'clock session, speaking on OData feed support in SharePoint 2013. It's a pretty complex and technical topic, but I had a great audience who asked some pretty thoughtful questions. These folks were obviously familiar with web services and custom solutions. I was definitely nervous that I wouldn't be able to articulate some of the concepts, so it was great to see the lightbulbs lighting up in the room as we worked through the material.

I really wanted to be able to show an end-to-end demo where we could implement a live OData service on the server side while implementing an SP 2013 app or solution to consume the data feed. But SharePoint 2013 being SharePoint 2013, my virtual machine looked like hot garbage on a sunny day. (Note to self: get company to pay for a laptop with 32GB of memory.) Luckily, there are several live feeds available for consumption, so it may have been a hodgepodge, but we managed to cover the high points.

Anil presented on 10 things administrators need to know about when moving to SharePoint 2013, but in the same 10 o'clock time as I did, so I didn't get a chance to see his talk. He said that his session went well, but I wish I could have sat in on it. Even developers need to learn about the admin side of the house, right?

Robert presented just after lunch on branding and styling changes between SharePoint 2010 and 2013. He's a branding master, so his audience definitely got their money's worth out of the presentation. The audience seemed to be about half developers/branding folks, and half end users, so it was a challenge to keep everyone engaged as the discussions veered from the technical to the practical, but Robert was up to the task.

Vijay and Robert presented in the last session of the day, in the same small conference room as I did. Everyone was kind of worn out by the end of the day, so the crowd was small and quiet.

After some photos, it was time to Adjourn to SharePint down the street for some beverages and billiards. It was great to see all the familiar faces again and get some more experience doing conference talks. I'll definitely be putting something together for SPS New York.

Files: