Single-Page Applications (SPAs) in SharePoint Using SPServices – Part 4 – GetListItemChangesSinceToken

As I mentioned in the last part of the series, when we build a Single Page Application (SPA), we’ll usually want to keep the data we’re displaying up to date. GetListItems and GetListItemChanges are two of the operations that can help with this, but the more robust option is GetListItemChangesSinceToken (MSDN documentation). In fact, it’s becoming my favorite list-data-getting operation, even over the rock solid GetListItems.

GetListItems is great for simply grabbing items from a list, but if you want to monitor the list for changes efficiently, you’ll need GetListItemChanges and/or GetListItemChangesSinceToken. Of the two, GetListItemChangesSinceToken is the far more powerful. In fact, once you start using it, you may well stop using GetListItems altogether.

[important]GetListItemChanges and GetListItemChangesSinceToken do not work in versions of SPServices before 2013.02.[/important]

(I wish I had gotten these two operations working long ago, now that I realize how powerful they are.)

In this post, let’s take a look at the GetListItemChangesSinceToken operation.

GetListItemChangesSinceToken

GetListItemChangesSinceToken is clearly related to its siblings, GetListItems and GetListItemChanges, with a cross between the two of their characteristics, plus some extra goodness. We can be very specific about what we request, as with GetListItems, but we also can decide to only receive changes since a specific database token (more on this below).

[webURL]

See the GetListItems post in this series.

listName

See the GetListItems post in this series.

viewName

See the GetListItems post in this series.

CAMLQuery

See the GetListItems post in this series.

CAMLViewFields

See the GetListItems post in this series.

CAMLRowLimit

See the GetListItems post in this series.

CAMLQueryOptions

See the GetListItems post in this series. Note that the MSDN documentation for GetListItemChangesSinceToken provides far more parameters than the MSDN documentation for GetListItems. Most (if not all – I haven’t tested everything) of these parameters work with GetListItems as well.

changeToken

A string that contains the change token for the request. For a description of the format that is used in this string, see Overview: Change Tokens, Object Types, and Change Types. If null is passed, all items in the list are returned.

contains

A Contains element that defines custom filtering for the query and that can be assigned to a System.Xml.XmlNode object, as in the following example.

<Contains>
  <FieldRef Name="Status"/>
  <Value Type="Text">Complete</Value>
</Contains>

This parameter can contain null.

In simpler terms, this can be a snippet of CAML to add an additional filter to the request.

Note

Contrary to the statements above about passing nulls for changeToken and contains, you cannot do so from the client (it may be allowable in server side code, but I’m not sure). If you pass an empty node in your SOAP request, the request will fail. SPServices – as of version 2013.02 – will handle empty values for you. This is the bug I fixed.

GetListItemChangesSinceToken Returns

In your first call to GetListItemChangesSinceToken, you won’t have a token yet. Or, you may choose not to pass a token that you do have. In that case, you get a robust amount of information returned to you.

The simplest call looks like this:

$().SPServices({
  operation: "GetListItemChangesSinceToken",
  listName: "Sales Opportunities"
});

Here, I’m just asking for the information for the Sales Opportunities list; I’m not providing any parameter values at all. On that first call, the results will look something like this (inside the SOAP envelope and GetListItemChangesSinceTokenResult):

<listitems xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema" MinTimeBetweenSyncs="0" RecommendedTimeBetweenSyncs="180" MaxBulkDocumentSyncSize="500" AlternateUrls="http://204.144.120.200/,http://www.sympraxisconsulting.com/,http://172.29.4.39/,http://sympraxisconsulting.com/" EffectivePermMask="FullMask">
  <Changes LastChangeToken="1;3;37920121-19b2-4c77-92ff-8b3e07853114;635243604504600000;33126">
    <List DocTemplateUrl="" DefaultViewUrl="/Intranet/JQueryLib/Lists/Sales Opportunities/AllItems.aspx" MobileDefaultViewUrl="" ID="{37920121-19B2-4C77-92FF-8B3E07853114}" Title="Sales Opportunities" Description="" ImageUrl="/_layouts/images/itgen.gif" Name="{37920121-19B2-4C77-92FF-8B3E07853114}" BaseType="0" FeatureId="00bfea71-de22-43b2-a848-c05709900100" ServerTemplate="100" Created="20090825 06:24:48" Modified="20131216 03:52:25" LastDeleted="20120627 07:54:25" Version="362" Direction="none" ThumbnailSize="" WebImageWidth="" WebImageHeight="" Flags="612372480" ItemCount="7" AnonymousPermMask="0" RootFolder="/Intranet/JQueryLib/Lists/Sales Opportunities" ReadSecurity="1" WriteSecurity="1" Author="3" EventSinkAssembly="" EventSinkClass="" EventSinkData="" EmailInsertsFolder="" EmailAlias="" WebFullUrl="/Intranet/JQueryLib" WebId="42f65d3f-343d-4627-a9a3-abf3d4d6491f" SendToLocation="" ScopeId="c8b78fea-7952-433a-be20-cda628ea6cbb" MajorVersionLimit="0" MajorWithMinorVersionsLimit="0" WorkFlowId="" HasUniqueScopes="False" AllowDeletion="True" AllowMultiResponses="False" EnableAttachments="True" EnableModeration="False" EnableVersioning="False" Hidden="False" MultipleDataList="False" Ordered="False" ShowUser="True" EnableMinorVersion="False" RequireCheckout="False">
      <Fields>
        <Field ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Type="Text" Name="Title" DisplayName="Title" Required="TRUE" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Title" FromBaseType="TRUE" ColName="nvarchar1"/>
        <Field Name="Lead_x0020_Source" FromBaseType="FALSE" Type="MultiChoice" DisplayName="Lead Source" Required="TRUE" FillInChoice="TRUE" ID="{0ab3481c-a7b4-432f-8292-c1617744a167}" Version="6" StaticName="Lead_x0020_Source" SourceID="{f4ebb20a-91e0-4306-997d-208e1d1920b7}" ColName="ntext3" RowOrdinal="0">
          <CHOICES>
            <CHOICE>Newspaper Advertising</CHOICE>
            <CHOICE>Web Site</CHOICE>
            <CHOICE>Personal Referral</CHOICE>
          </CHOICES>
        </Field>
        <Field Type="Currency" DisplayName="Potential Value" Required="FALSE" Decimals="2" LCID="1033" ID="{e9058e0c-b2e8-4af6-882c-9551a5f21451}" SourceID="{f4ebb20a-91e0-4306-997d-208e1d1920b7}" StaticName="Potential_x0020_Value" Name="Potential_x0020_Value" ColName="float1" RowOrdinal="0" Version="1">
          <Default>0</Default>
        </Field>
...
      <RegionalSettings>
        <Language>1033</Language>
        <Locale>1033</Locale>
        <AdvanceHijri>0</AdvanceHijri>
        <CalendarType>1</CalendarType>
        <Time24>False</Time24>
        <TimeZone>300</TimeZone>
        <SortOrder>2070</SortOrder>
        <Presence>True</Presence>
      </RegionalSettings>
      <ServerSettings>
        <ServerVersion>12.0.0.6421</ServerVersion>
        <RecycleBinEnabled>True</RecycleBinEnabled>
        <ServerRelativeUrl>/Intranet/JQueryLib</ServerRelativeUrl>
      </ServerSettings>
    </List>
  </Changes>
  <rs:data ItemCount="7">
    <z:row ows_Attachments="1" ows_LinkTitle="Tue Feb 14 15:58:39 EST 2012" ows_Modified="2013-12-11 10:04:38" ows_Author="3;#Marc D Anderson" ows_Lead_x0020_Source=";#Newspaper Advertising;#" ows_Potential_x0020_Value="12.6100000000000" ows_Lead_x0020_Date="2013-04-20 00:00:00" ows_Country="1;#United States" ows_Region="1;#Northeast" ows_State="2;#Rhode Island" ows_City="706;#Providence" ows_Course_x0020_Title="1;#ss" ows_System="3;#Alienware" ows_StateID="North Dakota" ows_Domain="7;#baloney.com" ows_Notes="&lt;div&gt;&lt;/div&gt;" ows_Web_x0020_Service_x0020_Operatio="13;#GetList" ows_Radio_x0020_Buttons="C" ows_Sales_x0020_Rep="104;#Marc Anderson;#89;#Eric Mullerbeck;#91;#Renzo Grande" ows_a_x0020_b_x0020_bcb_x0020_n="" ows_CalcTest="float;#151.320000000000" ows_CalcTestDate="datetime;#2009-08-25 14:24:48" ows_CalcTestYesNo="boolean;#1" ows_YesNo="1" ows_UpdateList_Field_Sun_x0020_Oct_x="lk;k;lk;lk" ows__Level="1" ows_UniqueId="5;#{3345A2C2-0B2D-4D79-9CB8-4FEFA993F5FA}" ows_FSObjType="5;#0" ows_Created_x0020_Date="5;#2009-08-25 14:24:48" ows_Title="Tue Feb 14 15:58:39 EST 2012" ows_Created="2009-08-25 14:24:48" ows_ID="5" ows_owshiddenversion="256" ows_FileLeafRef="5;#5_.000" ows_FileRef="5;#Intranet/JQueryLib/Lists/Sales Opportunities/5_.000" ows__ModerationStatus="0"/>
...
  </rs:data>
</listitems>

Yes, that is a heck of a lot of XML, but it’s not even all of it (note the ellipses). This may seem like overkill in many cases, and it may well be. In those cases, GetListItems is still the best way to go. But in the cases where you want to build an SPA, GetListItemsWithToken may be the ticket.

Like I said, you get a lot of robust information. Here are some more details on what’s there:

  • A bunch of info about what frequency and size requests are allowed. Generally you won’t need to care about these values, but they are there when you get to that point.
  • AlternateUrls – These are the URLs for each access path you might take to your list. Sweet! (The security folks will probably blanch at this.)
  • In any call to GetListItemChangesSinceToken we get a new value for LastChangeToken whether we have passed a changeToken or not. This token isn’t something we’re supposed to pick apart or understand; it represents a certain point in the database life. If you really want to understand what the pieces of the token mean, you can check out the article Overview: Change Tokens, Object Types, and Change Types. You’ll want to grab that value, as you will use it in subsequent calls. You can decipher what each part of it means from the documentation, but you really shouldn’t care. Basically, it’s *like* a timestamp, but in database terms.
  • The full list schema. You may not ever need this, but it’s great in cases where you find yourself doing a GetList/GetListItems pair of calls to set up further work in your script.
  • Server information (lines 17-31 above). This is really cool. I’ve never been able to figure out a consistent way to determine the language and locale settings, the time zone offset, etc. from the client, but here they are. Keep in mind that these are the *server* settings, not the client settings.
  • All of the list items. With large lists (not so large that they hit the 5000 item throttling limit – that’s another story entirely, and another post), you probably don’t want to get all of the list items. That’s where the contains parameter comes in handy. You might make the first call specifying that you only want the item with ID=1 or only items where the Title is null (generally this will result in no items) or something.

In the next call, you’re probably going to pass in the LastChangeToken value from the first call:

$().SPServices({
  operation: "GetListItemChangesSinceToken",
  listName: "Sales Opportunities",
  changeToken: "1;3;37920121-19b2-4c77-92ff-8b3e07853114;635243604504600000;33126"
});

If there have been no changes, the results will look something like this. Nice and simple.

<listitems MinTimeBetweenSyncs="0" RecommendedTimeBetweenSyncs="180" MaxBulkDocumentSyncSize="500" AlternateUrls="http://204.144.120.200/,http://www.sympraxisconsulting.com/,http://172.29.4.39/,http://sympraxisconsulting.com/" EffectivePermMask="FullMask" xmlns:rs="urn:schemas-microsoft-com:rowset">
<Changes LastChangeToken="1;3;37920121-19b2-4c77-92ff-8b3e07853114;635175524963570000;31260"> </Changes>
<rs:data ItemCount="0"></rs:data>
</listitems>

If there have been changes to items in the list, the results will look something like this:

<listitems MinTimeBetweenSyncs="0" RecommendedTimeBetweenSyncs="180" MaxBulkDocumentSyncSize="500" AlternateUrls="http://204.144.120.200/,http://www.sympraxisconsulting.com/,http://172.29.4.39/,http://sympraxisconsulting.com/" EffectivePermMask="FullMask" xmlns:rs="urn:schemas-microsoft-com:rowset">
<Changes LastChangeToken="1;3;37920121-19b2-4c77-92ff-8b3e07853114;635175524963570000;31260"> </Changes>
<rs:data ItemCount="2">
	<z:row ows_Attachments="0" ows_LinkTitle="MyItem1" ows_MetaInfo="3;#" ows__ModerationStatus="0"
		ows__Level="1" ows_Title="MyItem1" ows_ID="3" ows_owshiddenversion="2"
		ows_UniqueId="3;#{9153FDD3-7C00-47E9-9194-956BB20AAA8D}" ows_FSObjType="3;#0"
		ows_Created_x0020_Date="3;#2007-08-31T21:34:59Z" ows_Created="2007-08-31T21:34:59Z"
		ows_FileLeafRef="3;#3_.000" ows_FileRef="3;#sites/MyWebSite/Lists/MyList/3_.000"
		ows_ServerRedirected="0" />
	<z:row ows_Attachments="0" ows_LinkTitle="MyItem2" ows_MetaInfo="5;#" ows__ModerationStatus="0"
		ows__Level="1" ows_Title="MyItem2" ows_ID="5" ows_owshiddenversion="3"
		ows_UniqueId="5;#{5BDBB1C0-194D-4878-B716-E397B0C1318C}" ows_FSObjType="5;#0"
		ows_Created_x0020_Date="5;#2007-08-31T21:43:23Z" ows_Created="2007-08-31T21:43:23Z"
		ows_FileLeafRef="5;#5_.000" ows_FileRef="5;#sites/MyWebSite/Lists/MyList/5_.000"
		ows_ServerRedirected="0" />
</rs:data>
</listitems>

If there have been any item deletions, the Changes node will look a little different (you may see deletes, update, and adds depending on what’s been going on and how long it’s been since you asked):

<Changes LastChangeToken="1;3;641d61d7-b03e-4078-9e6c-379fa0208d6f;635243635233730000;33127">
  <Id ChangeType="Delete">1</Id>
</Changes>

This is just about the only place I know of where you can get information about deletes from the client side. Generally the only way to “catch” deletes is to write an event receiver to run on the server. Of course, you’ll only see the deletes since the last time you passed in a token value, but it allows you to reflect those deletes on the client.

If there have been any changes to the list settings (schema), we’ll get the schema again before the changed items. The results in this case look like what we get back from GetList and from the first call above. We receive everything we need to know to work with the list. For this reason, we would generally make a call to GetListItemChangesSinceToken first to get the list schema and all of the list items – at least those that match our filters – and then use either GetListItemChangesSinceToken, GetListItems, or GetListItemChanges for subsequent calls for our SPA.

Conclusion

As you can see, GetListItemsWithToken is incredibly powerful and returns a wealth of information. In fact, I’m not sure that you could get all of this from a REST call, but probably from CSOM. These old, crufty SOAP Web Services still have their place in the world.

Even better, they can provide you with what you need to get going on SPAs of you own. Hopefully you’re starting to see the possibilities.

jQuery Library for SharePoint Web Services (SPServices) 2013.02 Released

Yesterday I released SPServices 2013.02.

Here are the release headlines:

  • Improved compatibility with SharePoint 2013, whether on premises or on Office365, especially in the value-added functions (SPCascadeDropdowns, SPDisplayRelatedInfo, etc.)
  • Better API stability, including fixes for GetListItemChanges, GetListItemChangeSinceToken, and others
  • Bug fixes and performance improvements

While this is primarily a maintenance release, if you are using an earlier version of SPServices, I strongly suggest that you upgrade to this version, as you will see some performance improvements and there is some new functionality. Thanks to everyone who downloaded the beta and provided feedback.

Both releases in 2013 (2013.01 and 2013.02) work equally well – at least as best as I have been able to ensure – with SharePoint 2007, 2010, and 2013. My next planned release version will be 2014.01, which should make it obvious that the primary version number in my recent scheme is just the year of release.

One thing I wanted to mention that’s old news, but some people may have missed it. By far the most exciting thing in the 2013.01 release was jQuery promises, or deferred objects. All calls to the core operations return jQuery promises as of 2013.01. If you’re interested in increasing the efficiency of your code and getting ready for the way you are likely to work with the REST Web Services in SharePoint 2013, you should get familiar with using promises.

There are other goodies in this release, and you can see the full list of enhancements and improvements on the download page. Note the link to the Issue Tracker items for this release. For posterity, here are links to the release notes showing all the fixes and improvements from the Issue Tracker on Codeplex.

New Functionality

Alpha Issue Tracker Item Function Description
ALPHA5 10195 $().SPServices.SPGetQueryString Case insensitive SPGetQueryString?

Bug Fixes and Efficiency

Alpha Issue Tracker Item Function Description
ALPHA1 10152 $().SPServices.SPCascadeDropdown Multi-Select Lookup Cascading Dropdown’s Not Working
ALPHA1 10153 $().SPServices.SPComplexToSimpleDropdown Make SPComplexToSimpleDropdown Work in 2013 Publishing Pages
ALPHA1 10144 $().SPServices.SPXmlToJson userToJsonObject userId broken in 2013.01
ALPHA1 10146 $().SPServices ResolvePrincipals with addToUserInfoList=true requires SOAPAction
ALPHA2 10184 $().SPServices Lists.GetListItemChangeSinceToken operation input parameters not defined
ALPHA2 10183 $().SPServices Webs.GetColumns – Passing Unneeded webUrl Parameter
ALPHA2 10148 $().SPServices async on version 2013.01
ALPHA2 10177 $().SPServices First 2 arguments for the of addToPayload method for the GetTermSets are incorrect
ALPHA2 10154 $().SPServices SPCascadeDropdown: matchOnId not working with multi-select parents
ALPHA3 10165 $().SPServices SPServices() uses wrong server relative URL on SP2010
ALPHA3 10162 $().SPServices.SPRedirectWithID Bug in SPRedirectWithID
ALPHA3 10067 $().SPServices.SPAutocomplete SPAutocomplete dropdown issue
ALPHA3 10167 $().SPServices Caching bug if making same call to different webURLs
ALPHA4 10200 $().SPServices SPConvertDateToISO invalid TZD
ALPHA5 10147 $().SPServices.SPAutocomplete SPServices / jQuery ID Selectors in SP 2013, SPAutoComplete
ALPHA5 10189 $().SPServices.SPGetCurrentUser SPGetCurrentUser not being async
ALPHA5 10189 $().SPServices.SPGetCurrentUser SPGetCurrentUser data on iPhone
BETA2 10206 $().SPServices.SPCascadeDropdowns Multi-Select Dropdown Controls Don’t Have Name Attributes
BETA3 NA $().SPServices.SPDisplayRelatedInfo Fixed issues with lookup column handling

From SharePoint Magazine: Variations in Multiselect Controls in Different SharePoint Language Versions

This post originally appeared in SharePoint Magazine on December 23, 2010. SharePoint Magazine

Consistency in one’s development practices is a critical part of generating reliable, maintainable code. Most large software contains some inconsistencies, but in your development efforts with SharePoint, you should try your best not to introduce any inconsistencies of your own.

In this article, I’ll show you the effect of one of SharePoint’s little inconsistencies and why it was an issue in my development of SPServices.

If you’ve ever tried to implement jQuery in multi-language SharePoint solutions, you really need to pay attention.

Recently, I had to fix something in SPServices for someone who was using the German version of SharePoint 2010. The issue was with the way some Document Object Model (DOM) elements are identified in the German version. In fact, I’ve run into these inconsistencies before. These inconsistencies can add up to larger bugs not just for people like me who are trying to understand the DOM to make script work, but they probably contribute to issues that arise from installing service packs, hotfixes, and so on.

Whomever did the translations internally to some of the different languages wasn’t consistent in how they made some of the changes to things such as DOM element IDs. The specific case here is with multiselect columns. This inconsistency is the same whether you’re working with SharePoint 2007 or SharePoint 2010.

Anatomy of a Multiselect Column’s Markup

When SharePoint renders the rather complex controls for a multi-select column, it looks something like the image shown here. This is an example from a SharePoint 2010 installation, but it looks basically the same as it does in SharePoint 2007. The control is also the same whether you’re working with a NewForm or an EditForm.

image_thumb4The City column is a multiselect Lookup column. This seemingly simple type of column actually consists of many different DOM elements as well as some fairly complicated underlying JavaScript. (Yes, Virginia, SharePoint uses a lot of JavaScript right out of the box.)

Here is a snapshot of the DOM for the control at a high level:

SNAGHTML4bbbec4_thumbLet’s break that markup down into its components to see how it works:

SNAGHTML4bb3451_thumbBefore we even get to anything visual, there is a set of hidden INPUT elements that the scripts use to manage things. Each of the three INPUT elements have long IDs preceded by [long GUID-based stuff] and ending with the names in the following table:

MultiLookupPicker Used to store the currently selected set of values, using the odd separator ‘|t’, as in ’1|tAbington|t2|tActon|t3|tAcushnet’.
MultiLookupPicker_data Stores the full set of allowable values, like this ’1|tAbington|t |t |t2|tActon|t |t |t3|tAcushnet|t |t ‘ (and so on). It’s not clear why there are extra separators in this one.
MultiLookupPicker_initial Stores the initial value of the column as of page load.

Next comes a table that contains the visual elements of the control:

image_thumb5The left side of the control is a SELECT that has some script attached to its dblclick and change events. The important thing we need to know about this SELECT element is its title. In an English installation, the title=”City possible values”. It’s the column name followed by ‘ possible values’.

<TD class=ms-input>
  <SELECT style="WIDTH: 143px; HEIGHT: 125px; OVERFLOW: scroll" id=ctl00_m_g_856b69b7_94a3_499a_af52_e1789266b73a_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_SelectCandidate title="City possible values" ondblclick="GipAddSelectedItems(ctl00_m_g_856b69b7_94a3_499a_af52_e1789266b73a_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m); return false" multiple onchange=GipSelectCandidateItems(ctl00_m_g_856b69b7_94a3_499a_af52_e1789266b73a_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m); name=ctl00$m$g_856b69b7_94a3_499a_af52_e1789266b73a$ctl00$ctl05$ctl02$ctl00$ctl00$ctl04$ctl00$ctl00$SelectCandidate>
    <OPTION title=Abington selected value=1>Abington</OPTION>
    <OPTION title=Acton value=2>Acton</OPTION>
    <OPTION title=Acushnet value=3>Acushnet</OPTION>
    ...
    <OPTION title=Wrentham value=350>Wrentham</OPTION>
    <OPTION title=Yarmouth value=351>Yarmouth</OPTION>
  </SELECT>
</TD>

Next comes a 10-pixel-wide spacer TD. It’s not very interesting, but I mention it for completeness. Usually you’d probably do spacing like this in the CSS rather than having a dedicated TD for it.

<td style="padding-left: 10px;"/>

image_thumb6

Next come the two buttons. Both buttons have some script attached to their click event that manages the movement of the values between the left and right boxes.

<TD class=ms-input vAlign=center align=middle>
  <BUTTON id=ctl00_m_g_856b69b7_94a3_499a_af52_e1789266b73a_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_AddButton class=ms-ButtonHeightWidth onclick="GipAddSelectedItems(ctl00_m_g_856b69b7_94a3_499a_af52_e1789266b73a_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m); return false" type=submit>Add &gt;</BUTTON>
  <BR>
  <BR>
  <BUTTON id=ctl00_m_g_856b69b7_94a3_499a_af52_e1789266b73a_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_RemoveButton class=ms-ButtonHeightWidth disabled onclick="GipRemoveSelectedItems(ctl00_m_g_856b69b7_94a3_499a_af52_e1789266b73a_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m); return false" type=submit>&lt; Remove</BUTTON>
</TD>

image_thumb7

Now another 10-pixel-wide spacer TD:

image_thumb8

And finally, we have the right side box. Like the left side, this is also a SELECT with some script attached to its dblclick and change events. The important thing we need to know about this SELECT element to work with it is its title. In an English installation, title=”City selected values”. It’s the column name followed by ‘ selected values’.

<TD class=ms-input>
  <SELECT style="WIDTH: 143px; HEIGHT: 125px; OVERFLOW: scroll" id=ctl00_m_g_517395ec_085b_47db_b888_b55773e76d86_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_SelectResult title="City selected values" ondblclick="GipRemoveSelectedItems(ctl00_m_g_517395ec_085b_47db_b888_b55773e76d86_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m); return false" multiple onchange=GipSelectResultItems(ctl00_m_g_517395ec_085b_47db_b888_b55773e76d86_ctl00_ctl05_ctl02_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m); name=ctl00$m$g_517395ec_085b_47db_b888_b55773e76d86$ctl00$ctl05$ctl02$ctl00$ctl00$ctl04$ctl00$ctl00$SelectResult>
    <OPTION title=Abington value=1>Abington</OPTION>
    <OPTION title=Acton value=2>Acton</OPTION>
    <OPTION title=Acushnet value=3>Acushnet</OPTION>
  </SELECT>
</TD>

image_thumb9

There’s a lot more to this simple little thing that you might have expected, eh? And I haven’t even gone into how the underlying script works. In fact, that’s so complicated that I’m going to leave it for another time.

The Inconsistency

But back to the whole point of this article, which is the inconsistency across language versions of SharePoint. The key is to be able to find the important piece parts of the control in the DOM with SPServices (in my case) and be sure that we are working with the right one if there are multiple multiselect columns in the form. To do this, I have a function in SPServices called dropdownCtl. Its little purpose in life is to find a “dropdown” and return an object reference to it in the DOM as well as its “Type”. The functions SPCascadeDropdowns, SPDisplayRelatedInfo, and SPLookupAddNew in SPServices all use the dropdownCtl function to find the right elements in the DOM to work with.

One other tricky part is that the are three types of “dropdowns” we can have in the form. (I’m using “dropdowns” in quotes because it’s a little bit of a stretch to call the multiselect control a dropdown.) Each type has different representations in the DOM elements, so dropdownCtl needs to find the right element and also decide which type of element it is.

The first two types are what you usually would think about as dropdowns, and they are easier to find in the DOM. They are either a “simple SELECT,” which is rendered if there are fewer than 20 options for the column, or a “complex SELECT,” which SharePoint uses if there are 20 or more options. The “complex SELECT” is actually a hybrid INPUT/SELECT with some script to drive it.

In the case of a “multiselect dropdown,” it’s a little trickier. The best way to find the various elements in the page for a “multiselect dropdown” is to look for the SELECTs’ titles. This is where the translation differences come into play.

It’s understandable (perhaps) that the English titles should be translated into the other languages. What doesn’t make sense is that the format of those translations is quite different. Let’s look at what the City column’s right box would have as its title in three different languages:

English City selected values
Russian Выбранных значений: City
(this is for the possible values, but the point is the same)
German Ausgewählte Werte für “City”

As you can see, the pattern of the text varies, not just the words themselves. I don’t speak Russian or German, but it’s hard to imagine that it’s necessary to change the construct from ‘City selected values’ to ‘Selected values: City’ or ‘Selected values for “City”‘ in order to reflect some sort of nationalistic thinking.

In my code, I don’t want to care about what language is being used; I just want to be able to find the right title. By using different patterns, SharePoint makes this more difficult than it should be.

The Solution

Because of the differences between the titles in the different language versions of SharePoint, in my dropdownCtl function I have to have these three conditions:

// Multi-select: This will find the multi-select column control in English and most other languages sites where the Title looks like 'Column Name possible values'
} else if ((this.Obj = $("select[ID$='SelectCandidate'][Title^='" + colName + " ']")).html() != null) {
  this.Type = "M";
  // Multi-select: This will find the multi-select column control on a Russian site (and perhaps others) where the Title looks like 'Выбранных значений: Column Name'
} else if ((this.Obj = $("select[ID$='SelectCandidate'][Title$=': " + colName + "']")).html() != null) {
  this.Type = "M";
  // Multi-select: This will find the multi-select column control on a German site (and perhaps others) where the Title looks like 'Mögliche Werte für &amp;quot;Tops&amp;quot;.'
} else if ((this.Obj = $("select[ID$='SelectCandidate'][Title$='\"" + colName + "\".']")).html() != null) {
  this.Type = "M";

Obviously, I can handle this in my code, but every time I hear from someone who uses a different language version of SharePoint and sees a bug, I need to revisit what the pattern of the text is in that language.

The Moral of the Story

The moral of the story is that seemingly tiny decisions can actually make more significant differences. Consider very carefully why you want to change the style of something like an ID or markup that you’re rendering in a control because there can be ramifications later. We all want to leave a little bit of our personal style in our code, but try not to do so at the expense of reusability, interoperability, or consistency.


Postscript

Since I wrote this article back in 2010, I’ve made adjustments to handle additional languages. I’ve added Italian and I’ve recently been asked for a fix for Portuguese. Should you find another inconsistency, please let me know by posting in the SPServices Discussions on Codeplex.

Here’s what my DropdownCtl function looks like in version 2013.01:

// Find a dropdown (or multi-select) in the DOM. Returns the dropdown onject and its type:
// S = Simple (select);C = Compound (input + select hybrid);M = Multi-select (select hybrid)
function DropdownCtl(colName) {
  // Simple
  if ((this.Obj = $("select[Title='" + colName + "']")).length === 1) {
    this.Type = "S";
    // Compound
  } else if ((this.Obj = $("input[Title='" + colName + "']")).length === 1) {
    this.Type = "C";
    // Multi-select: This will find the multi-select column control in English and most other languages sites where the Title looks like 'Column Name possible values'
  } else if ((this.Obj = $("select[ID$='SelectCandidate'][Title^='" + colName + " ']")).length === 1) {
    this.Type = "M";
    // Multi-select: This will find the multi-select column control on a Russian site (and perhaps others) where the Title looks like 'Выбранных значений: Column Name'
  } else if ((this.Obj = $("select[ID$='SelectCandidate'][Title$=': " + colName + "']")).length === 1) {
    this.Type = "M";
    // Multi-select: This will find the multi-select column control on a German site (and perhaps others) where the Title looks like 'Mögliche Werte für &quot;Column name&quot;.'
  } else if ((this.Obj = $("select[ID$='SelectCandidate'][Title$='\"" + colName + "\".']")).length === 1) {
    this.Type = "M";
    // Multi-select: This will find the multi-select column control on a Italian site (and perhaps others) where the Title looks like "Valori possibili Column name"
  } else if ((this.Obj = $("select[ID$='SelectCandidate'][Title$=' " + colName + "']")).length === 1) {
    this.Type = "M";
  } else {
    this.Type = null;
  }
} // End of function DropdownCtl

Single-Page Applications (SPAs) in SharePoint Using SPServices – Part 3 – GetListItemChanges

In building a Single Page Application (SPA), we’ll usually want to keep the data we’re displaying up to date. You can probably think of many examples where you see this on the Web, but newsfeeds are a prime example. While we’re sitting on the page, we see newly posted content pop up, usually on the top of the feed. To do this, we can simply set up a call to run at fixed intervals using JavaScript’s timing functions setTimeout() or setInterval() to pull back the data.

If the content we want is in a list, it’s “expensive” to request all of the items at each interval. Instead, it’s much better to either cache the items if they rarely change – as is available in SPServices 0.7.2+, though the approach in 2013.01 is much improved – or to request only those items which have changed since the last request.

There are two operations which can help with this: GetListItemChanges and GetListItemChangesWithToken.

[important]GetListItemChanges and GetListItemChangesWithToken do not work in versions of SPServices before 2013.02.[/important]

In this post, let’s take a look at the GetListItemChanges operation.

GetListItemChanges

GetListItemChanges retrieves changes to the list since a specified date/time. The parameters are similar to those for GetListItems, with a few additions:

[webURL]

See the GetListItems post in this series.

listName

See the GetListItems post in this series.

viewFields

See the GetListItems post in this series.

since

A date/time specified in Coordinated Universal Time (UTC) ISO8601 format. What that means is that the date/time has to look like “2013-06-30T15:29:43Z”. That’s “YYYY-MM-DDThh:mm:ssZ”. The “Z” means “Zulu” time, or GMT. You can also provide time offsets, like “2013-06-30T15:29:43-05:00″ for Eastern (US) Time.

Because I’m a nice guy, I have a function in SPServices called SPConvertDateToISO to convert JavaScript date/time objects to the ISO8601 format.

contains

The contains parameter is much like the CAMLQuery parameter in GetListItems, but a little simpler. For instance, you may only be interested in list changes where the current user is the Author, in which case you’d pass:

contains: "<Eq><FieldRef Name='Author'/><Value Type='Integer'><UserID/></Value></Eq>",

If you don’t pass anything for contains, you’ll get back all of the changes since since.

Because the GetListItemChanges operation retrieves only changes in the list, the requests will return very slim results in all but the most active lists. However, the operation will prove valuable in our SPA development because it will allow us to keep our display current for the user with a minimum of fuss unless there have been changes to the underlying data.

GetListItemChanges Returns

When Paul Tavares (@paul_tavares) pointed out the GetListItemChangesWithToken operation a while back, I recalled that I never could get it running properly in my test environment back when I first wrapped it in SPServices. I wrote it off to either my own ineptitude or simply a bug on the Microsoft side (that wouldn’t be a first). At the time, it didn’t seem as though the operation offered enough benefit to chase it down any further.

I was wrong on several counts, of course. The bit about my ineptitude was the one part I was right about. When I wrapped the operation, I accepted two things about it that the documentation told me:

changeToken

A string that contains the change token for the request. For a description of the format that is used in this string, see Overview: Change Tokens, Object Types, and Change Types. If null is passed, all items in the list are returned.

contains

A Contains element that defines custom filtering for the query and that can be assigned to a System.Xml.XmlNode object, as in the following example.

<Contains>
   <FieldRef Name="Status"/>
   <Value Type="Text">Complete</Value>
</Contains>

This parameter can contain null.

The italicized, maroon parts were the issue. While it’s probably possible to pass null on the server side, there’s no such analogous value on the client that we can pass. Since everything we pass is text, generally an empty string will work in those operations where null is allowable. But not in this case.

This means that in all versions of SPServices until the alpha I’ve currently got posted, the operations GetListItemsChanges and GetListItemsChangesWithToken won’t work, no matter how hard you try. That’s because I passed empty strings for the elements above, which just throws an error.

All that said, GetListItemsChanges returns XML that looks the same as what GetListItems gives us.

<listitems xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
   xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
   xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema"
   xmlns="http://schemas.microsoft.com/sharepoint/soap/"
   TimeStamp="2013-11-14T16:30:36Z">
   <rs:data ItemCount="4">
      <z:row ows_Number_Field="6555.00000000000"
         ows_Created="2003-06-18T03:41:09Z"
         ows_ID="3" ows_owshiddenversion="3" />
      <z:row ows_Number_Field="78905456.0000000"
         ows_Created="2003-06-18T17:15:58Z"
         ows_ID="4" ows_owshiddenversion="2" />
         ...
   </rs:data>
</listitems>

There’s one subtle difference. Can you spot it? GetListItemChanges gives us one additional piece of data with the results, and that’s the TimeStamp value. This date/time value tells us when we requested the data, so we can use that value to inform the user when the data was last updated, should we want to.

Processing GetListItems Results

Processing the results from GetListItemChanges is just about the same as with GetListItems.

var out = "<ul>";
$().SPServices({
  operation: "GetListItemChanges",
  since: "2013-11-14T11:00:00-5:00",
  async: false,
  listName: "Announcements",
  CAMLRowLimit: 0,
  CAMLViewFields: "<ViewFields><FieldRef Name='Title' /></ViewFields>",
  completefunc: function (xData, Status) {
    var timeStamp = $(xData.responseXML).SPFilterNode("listitems").attr("TimeStamp");
    var itemCount = $(xData.responseXML).SPFilterNode("rs:data").attr("ItemCount");
    $(xData.responseXML).SPFilterNode("z:row").each(function() {
      out += "<li>" + $(this).attr("ows_Title") + "</li>";
    });
    out += "</ul>";
    $("#announcementsContainer").html(out);
  }
});

Here I’m making the call to GetListItemChanges to get all of the items in the Announcements list which have changed since 11am this morning in Boston.

Conclusion

GetListItemChanges is a great addition to our toolkit because it allows us to make requests for items in a list that have changed since a time we specify. While we could accomplish something similar with GetListItems by passing in a filter for Modified in the CAMLQuery, GetListItemChanges is built for exactly what we need, and I would hope that it is therefore more efficient on the server side.
is a workhorse, all right, but we need more for our SPA work. In the next three parts of the series, I’ll introduce its siblings: GetListItemChanges, GetListItemChangesWithToken, and UpdateListItems. These four operations together will help us to build a highly interactive SPA and provide valuable functionality for our users.

SPServices Passes 100,000 Total Downloads

Here are some numbers for the meaningless-but-fun-statistics bin. Codeplex collects a bunch of numbers for me, so I can preserve them here.

Yesterday, Sunday, October 27th, 2013, SPServices crossed the 100,000 total downloads threshold. I posted the first version of SPServices (SPServices 0.2.3) on Codeplex on Aug 19th, 2009 as an alpha. That means it’s taken 4 years, 2 months, and 9 days – that’s 1531 days – to reach 100,000 downloads. That’s over 65 downloads per day!

SPServices 100,000 DownloadsDuring that same period, SPServices has also had almost 3,000,000 page views – 2,945,339 to be exact. I like to think that this number is a testament to the quality of the documentation.

SPServices 3,000,000 Page ViewsThere have been over 3/4 million visits to the Codeplex site.

SPServices 3/4 Million VisitsIt’s been a fun 4 years, 2 months, and 9 days working on and supporting SPServices. I doubt it’s got another 4 years in it, but I look forward to many more months and downloads to come.

Yes, I do make it up in volume.