Monday, February 11, 2013

Setting field values using CSOM client side

A rather long time ago, I wrote some code to handle creation and updates of items when using Client Object Model.  This bit of code works very well however it's always made me feel like the API I've built had warts. Take a look for yourself.  This method allows for anyone to modify Lookup values as well as People Picker values.  Using this, you can also set multiple values without a problem.  That's the good...  The ugly is for this to work, it's necessary to tell spUtils the column type.  This ended up looking something like this:


/***************************************
Test 26 ~ updateListItems - Updates item's lookup and people picker column
***************************************/
spUtils.updateListItems({
listName : "Project Tasks",
updates : {
111 : { // the key is the item ID
"RelatedProject{L}" : spUtils.isoDate(),
"AssignedTo{P}" : 1,

     "Title" : "Hello, World!"
}
},
success : function() { debugger; }
});


// End codez


Notice the appended characters that represent the column type. This is fine but at the end of the day not very user friendly...  I've always thought I could do better.  This blog post will explore what I've found inside the SP Namespace and what my options are to fix it.

SP.Debug.js to the Rescue?

When looking at a listItem object in my debugger, I found a rather coy method.  It literally screamed at me: "Put me in the game coach!"... Totally looked over this one when building CRUD into spUtils.  With that said, I've made the change under the hood to use this method instead.

parseAndSetFieldValue: function(fieldName, value_) {ULS5Vl:;
        this.get_fieldValues()[fieldName] = value_;
        var $v_0 = new SP.ClientActionInvokeMethod(this, 'ParseAndSetFieldValue', [ fieldName, value_ ]);
        if ((this.get_context())) {
            this.get_context().addQuery($v_0);
        }
},


For posterity, here's the raw bits of the function call. Nothing too exciting here since it's really a wrapper for the ClientActionInvokeMethod. Come to think of it, what isn't a wrapper for ClientActionInvokeMethod inside SP.js?

Is parseAndSetFieldValue really up to snuff?

For me it was truly a magical moment coming across this method.  As of matter of fact, I made a note of it when I did to come back someday and put the spotlight directly on it...  As of a result, here are my findings in raw format ( I've highlighted the important pieces of XML ):


<!-- Test 1
[ "AssignedTo", ["DEV\\Administrator", "DEV\\spuser"] ]
XML sent to Server using Array of strings
-->

<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="14.0.0.0" LibraryVersion="14.0.4762.1000" ApplicationName="Javascript Library">
<Actions>
<ObjectPath Id="1" ObjectPathId="0" />
<ObjectPath Id="3" ObjectPathId="2" />
<ObjectPath Id="5" ObjectPathId="4" />
<ObjectPath Id="7" ObjectPathId="6" />
<ObjectIdentityQuery Id="8" ObjectPathId="6" />
<ObjectPath Id="10" ObjectPathId="9" />
<Method Name="ParseAndSetFieldValue" Id="11" ObjectPathId="9">
<Parameters>
<Parameter Type="String">Title</Parameter>
<Parameter Type="String">2013-02-11T20:10:13Z</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="12" ObjectPathId="9" />
<Query Id="13" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
</Properties>
</Query>
</Query>
<Method Name="ParseAndSetFieldValue" Id="14" ObjectPathId="9">
<Parameters>
<Parameter Type="String">AssignedTo</Parameter>
<Parameter Type="Array">
<Object Type="String">DEV\Administrator</Object>
<Object Type="String">DEV\spuser</Object>
</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="15" ObjectPathId="9" />
<Query Id="16" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
<Property Name="AssignedTo" ScalarProperty="true" />
</Properties>
</Query>
</Query>
<Method Name="ParseAndSetFieldValue" Id="17" ObjectPathId="9">
<Parameters>
<Parameter Type="String">RelatedProject</Parameter>
<Parameter Type="Number">2</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="18" ObjectPathId="9" />
<Query Id="19" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
<Property Name="AssignedTo" ScalarProperty="true" />
<Property Name="RelatedProject" ScalarProperty="true" />
</Properties>
</Query>
</Query>
</Actions>
<ObjectPaths>
<StaticProperty Id="0" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" />
<Property Id="2" ParentId="0" Name="Web" />
<Property Id="4" ParentId="2" Name="Lists" />
<Method Id="6" ParentId="4" Name="GetByTitle">
<Parameters>
<Parameter Type="String">Project Tasks</Parameter>
</Parameters>
</Method>
<Method Id="9" ParentId="6" Name="GetItemById">
<Parameters>
<Parameter Type="Number">42</Parameter>
</Parameters>
</Method>
</ObjectPaths>
</Request>

Result:
Request failed. Invalid look-up value. A look-up field contains invalid data. Please check the value and try again. 


<!-- Test 2 
[ "AssignedTo", "DEV\\Administrator, DEV\\spuser" ]
XML sent to Server using comma separated values inside a string.
-->

<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="14.0.0.0" LibraryVersion="14.0.4762.1000" ApplicationName="Javascript Library">
<Actions>
<ObjectPath Id="1" ObjectPathId="0" />
<ObjectPath Id="3" ObjectPathId="2" />
<ObjectPath Id="5" ObjectPathId="4" />
<ObjectPath Id="7" ObjectPathId="6" />
<ObjectIdentityQuery Id="8" ObjectPathId="6" />
<ObjectPath Id="10" ObjectPathId="9" />
<Method Name="ParseAndSetFieldValue" Id="11" ObjectPathId="9">
<Parameters>
<Parameter Type="String">Title</Parameter>
<Parameter Type="String">2013-02-11T20:16:15Z</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="12" ObjectPathId="9" />
<Query Id="13" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
</Properties>
</Query>
</Query>
<Method Name="ParseAndSetFieldValue" Id="14" ObjectPathId="9">
<Parameters>
<Parameter Type="String">AssignedTo</Parameter>
<Parameter Type="String">DEV\Administrator; DEV\spuser</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="15" ObjectPathId="9" />
<Query Id="16" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
<Property Name="AssignedTo" ScalarProperty="true" />
</Properties>
</Query>
</Query>
</Actions>
<ObjectPaths>
<StaticProperty Id="0" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" />
<Property Id="2" ParentId="0" Name="Web" />
<Property Id="4" ParentId="2" Name="Lists" />
<Method Id="6" ParentId="4" Name="GetByTitle">
<Parameters>
<Parameter Type="String">Project Tasks</Parameter>
</Parameters>
</Method>
<Method Id="9" ParentId="6" Name="GetItemById">
<Parameters>
<Parameter Type="Number">42</Parameter>
</Parameters>
</Method>
</ObjectPaths>
</Request>

Result:
Request failed. Invalid data has been used to update the list item. The field you are trying to update may be read only. 

<!-- Test 3
[ "AssignedTo", "DEV\\Administrator;#DEV\\spuser" ]
XML sent to Server using the old semi-colon bang delimiter.
-->

<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="14.0.0.0" LibraryVersion="14.0.4762.1000" ApplicationName="Javascript Library">
<Actions>
<ObjectPath Id="1" ObjectPathId="0" />
<ObjectPath Id="3" ObjectPathId="2" />
<ObjectPath Id="5" ObjectPathId="4" />
<ObjectPath Id="7" ObjectPathId="6" />
<ObjectIdentityQuery Id="8" ObjectPathId="6" />
<ObjectPath Id="10" ObjectPathId="9" />
<Method Name="ParseAndSetFieldValue" Id="11" ObjectPathId="9">
<Parameters>
<Parameter Type="String">Title</Parameter>
<Parameter Type="String">2013-02-11T20:28:42Z</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="12" ObjectPathId="9" />
<Query Id="13" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
</Properties>
</Query>
</Query>
<Method Name="ParseAndSetFieldValue" Id="14" ObjectPathId="9">
<Parameters>
<Parameter Type="String">AssignedTo</Parameter>
<Parameter Type="String">DEV\Administrator;#DEV\spuser</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="15" ObjectPathId="9" />
<Query Id="16" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
<Property Name="AssignedTo" ScalarProperty="true" />
</Properties>
</Query>
</Query>
</Actions>
<ObjectPaths>
<StaticProperty Id="0" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" />
<Property Id="2" ParentId="0" Name="Web" />
<Property Id="4" ParentId="2" Name="Lists" />
<Method Id="6" ParentId="4" Name="GetByTitle">
<Parameters>
<Parameter Type="String">Project Tasks</Parameter>
</Parameters>
</Method>
<Method Id="9" ParentId="6" Name="GetItemById">
<Parameters>
<Parameter Type="Number">42</Parameter>
</Parameters>
</Method>
</ObjectPaths>
</Request>

Result:
Request failed. Invalid look-up value.  A look-up field contains invalid data. Please check the value and try again. 

<!-- Test 4 
[ "AssignedTo", "DEV\\Administrator; DEV\\spuser" ]
XML sent to Server using text similar to typing into the control manually.
-->

<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="14.0.0.0" LibraryVersion="14.0.4762.1000" ApplicationName="Javascript Library">
<Actions>
<ObjectPath Id="1" ObjectPathId="0" />
<ObjectPath Id="3" ObjectPathId="2" />
<ObjectPath Id="5" ObjectPathId="4" />
<ObjectPath Id="7" ObjectPathId="6" />
<ObjectIdentityQuery Id="8" ObjectPathId="6" />
<ObjectPath Id="10" ObjectPathId="9" />
<Method Name="ParseAndSetFieldValue" Id="11" ObjectPathId="9">
<Parameters>
<Parameter Type="String">Title</Parameter>
<Parameter Type="String">2013-02-11T20:32:14Z</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="12" ObjectPathId="9" />
<Query Id="13" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
</Properties>
</Query>
</Query>
<Method Name="ParseAndSetFieldValue" Id="14" ObjectPathId="9">
<Parameters>
<Parameter Type="String">AssignedTo</Parameter>
<Parameter Type="String">DEV\Administrator; DEV\spuser</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="15" ObjectPathId="9" />
<Query Id="16" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
<Property Name="AssignedTo" ScalarProperty="true" />
</Properties>
</Query>
</Query>
</Actions>
<ObjectPaths>
<StaticProperty Id="0" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" />
<Property Id="2" ParentId="0" Name="Web" />
<Property Id="4" ParentId="2" Name="Lists" />
<Method Id="6" ParentId="4" Name="GetByTitle">
<Parameters>
<Parameter Type="String">Project Tasks</Parameter>
</Parameters>
</Method>
<Method Id="9" ParentId="6" Name="GetItemById">
<Parameters>
<Parameter Type="Number">42</Parameter>
</Parameters>
</Method>
</ObjectPaths>
</Request>

Result:
Request failed. Invalid data has been used to update the list item. The field you are trying to update may be read only. 


<!-- Test 5
[ "AssignedTo", 1 ]
XML sent to Server using a single user id.  Works as a string as well.
-->

<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="14.0.0.0" LibraryVersion="14.0.4762.1000" ApplicationName="Javascript Library">
<Actions>
<ObjectPath Id="1" ObjectPathId="0" />
<ObjectPath Id="3" ObjectPathId="2" />
<ObjectPath Id="5" ObjectPathId="4" />
<ObjectPath Id="7" ObjectPathId="6" />
<ObjectIdentityQuery Id="8" ObjectPathId="6" />
<ObjectPath Id="10" ObjectPathId="9" />
<Method Name="ParseAndSetFieldValue" Id="11" ObjectPathId="9">
<Parameters>
<Parameter Type="String">Title</Parameter>
<Parameter Type="String">2013-02-11T20:37:00Z</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="12" ObjectPathId="9" />
<Query Id="13" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
</Properties>
</Query>
</Query>
<Method Name="ParseAndSetFieldValue" Id="14" ObjectPathId="9">
<Parameters>
<Parameter Type="String">AssignedTo</Parameter>
<Parameter Type="Number">1</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="15" ObjectPathId="9" />
<Query Id="16" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
<Property Name="AssignedTo" ScalarProperty="true" />
</Properties>
</Query>
</Query>
</Actions>
<ObjectPaths>
<StaticProperty Id="0" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" />
<Property Id="2" ParentId="0" Name="Web" />
<Property Id="4" ParentId="2" Name="Lists" />
<Method Id="6" ParentId="4" Name="GetByTitle">
<Parameters>
<Parameter Type="String">Project Tasks</Parameter>
</Parameters>
</Method>
<Method Id="9" ParentId="6" Name="GetItemById">
<Parameters>
<Parameter Type="Number">42</Parameter>
</Parameters>
</Method>
</ObjectPaths>
</Request>

Result:
Holy shit! It works....


<!-- Test 6
[ "AssignedTo", [ 1, 17 ] ]
XML sent to Server using an array of numbers.
-->

<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="14.0.0.0" LibraryVersion="14.0.4762.1000" ApplicationName="Javascript Library">
<Actions>
<ObjectPath Id="1" ObjectPathId="0" />
<ObjectPath Id="3" ObjectPathId="2" />
<ObjectPath Id="5" ObjectPathId="4" />
<ObjectPath Id="7" ObjectPathId="6" />
<ObjectIdentityQuery Id="8" ObjectPathId="6" />
<ObjectPath Id="10" ObjectPathId="9" />
<Method Name="ParseAndSetFieldValue" Id="11" ObjectPathId="9">
<Parameters>
<Parameter Type="String">Title</Parameter>
<Parameter Type="String">2013-02-11T20:40:36Z</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="12" ObjectPathId="9" />
<Query Id="13" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
</Properties>
</Query>
</Query>
<Method Name="ParseAndSetFieldValue" Id="14" ObjectPathId="9">
<Parameters>
<Parameter Type="String">AssignedTo</Parameter>
<Parameter Type="Array">
<Object Type="Number">17</Object>
<Object Type="Number">1</Object>
</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="15" ObjectPathId="9" />
<Query Id="16" ObjectPathId="9">
<Query SelectAllProperties="false">
<Properties>
<Property Name="Title" ScalarProperty="true" />
<Property Name="AssignedTo" ScalarProperty="true" />
</Properties>
</Query>
</Query>
</Actions>
<ObjectPaths>
<StaticProperty Id="0" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" />
<Property Id="2" ParentId="0" Name="Web" />
<Property Id="4" ParentId="2" Name="Lists" />
<Method Id="6" ParentId="4" Name="GetByTitle">
<Parameters>
<Parameter Type="String">Project Tasks</Parameter>
</Parameters>
</Method>
<Method Id="9" ParentId="6" Name="GetItemById">
<Parameters>
<Parameter Type="Number">42</Parameter>
</Parameters>
</Method>
</ObjectPaths>
</Request>

Result:
Well, it doesn't fail... However the column is set to null.


Well that just sucks now doesn't it?  Using that method, I can ONLY use the user ID AND ONLY one user ID.  What a waste of time.  The method should really be called: parseValuesAndWasteMatthewsTime().

What about GetList?

I could query the current site, cache all of the list information and then figure out the column types on the fly.
While I know I could pull this off, I really don't think it's a great option.  CSOM is asynchronous in nature, which means I'd have to nest everything AFTER the initial call to get the list information.  That would be worse than what is already there...

What's next?

Since this inefficiency cannot be overcome in SP2010, I'm going to press on with the API I currently have because it simply works...  I'll continue to look for better ways to do this, as this is primarily why I've never documented spUtils.  I figured the API would change once I found a different way and didn't want to deal with the overhead of dealing with that.

Well since I feel like I've given the SP Namespace a fair shake, it's time to clean up the code and start vetting it for usage within SP2013.  Look for much more capabilities to be baked into this coming soon!

Example: spUtils.startWorkflow();  // This will not use web services to accomplish this. I'm trying to rely on SP.js for everything.

Happy coding!

No comments: