quarta-feira, 6 de fevereiro de 2019

"No exact match" - the SharePoint Online error that went under the radar

Up until today, Feb 6th 2019, in order to update a multi-value Person or Group (aka "multiuser") field, something along these lines -

var ctx = SP.ClientContext.get_current();
var list = ctx.get_web().get_lists().getByTitle('someList');

var u1 = new SP.FieldUserValue();
u1.set_lookupId(1); //1 being a known user ID obtained, for example, from the User Information List

var u2 = new SP.FieldUserValue();
u1.set_lookupId(2); //similar to u1

var itemCreateInfo = new SP.ListItemCreationInformation();
var item = list addItem(itemCreateInfo);
item.set_item('Title', 'My New Item!');
item.set_item('MUser', [u1, u2]); //MUSer being a Person or Group column allowing multiple values
item.update();

ctx.executeQueryAsync(function() { console.log('OK!'); }, function(s, a) { console.log(a.get_message()); });

- worked fine on a classic-experience site, in SPO. But for some reason, it just stopped working, with a.get_message() returning something like "no exact match was found" (or "we couldn't find an exact match"). Oddly enough, this error message, while somewhat cryptical, did provide some indication of what was going wrong. Using the out-of-the-box form, it was still possible to save multiple values to the (MUSer) column, it just didn't work when using CSOM/JSOM. And when the column value was retrieved for a previously created item - using ...getItemById(#) - the resulting SP.FieldUserValue array elements, did have something that u1 and u2, from the example above, (as expected) did not have: the LoginName and Email properties filled.

At this point, it was worth trying to get these filled besides just the Id before executing the request, but both cannot be set for FieldUserValue, as they are private properies. Luckily, SP.FieldUserValue.fromUser([loginame or email here]) was available.

As it turns out, the lack of one of those two properties was the cause of the problem - 
var u1 = SP.FieldUserValue.fromUser('john.doe@acme.com') is the workaround - but there is still no explanation to what changed in SharePoint Online that led to this (and why!). Even stranger is the fact the so very few people seem to have noticed it.

P.S.: This is a similar problem, but why is a SharePoint 2007 (on-premise!) issue popping up 10 years later in SharePoint Online, is a question only Microsoft can answer.


Soundtrack for this finding:

Can't say I was inspired by music when I cracked this, but I did listen to "Run To The Hills" (Iron Maiden) in the end, just for kicks.

quinta-feira, 1 de dezembro de 2016

My take on the "Cannot add the specified assembly to the global assembly cache (...)" issue

I got this when updating a WSP, on a SharePoint 2013 server. Tried the suggested IISReset, Timer Service restart... no luck. This however worked like a charm:

[System.EnterpriseServices.Internal.Publish] $publish = new-object System.EnterpriseServices.Internal.Publish
$publish.GacRemove("c:\windows\microsoft.net\assembly\remaining-path-to-trouble.dll")

terça-feira, 16 de dezembro de 2014

Publishing content types, 'site could not be retrieved'

"Job: MetadataSubscriberTimerJob skipped processing a site 'http://meSite' which could not be retrieved."

I got this while trying to push published content types, into a site collection that I had previously moved to a different content database, but without performing the necessary IISReset.

Even after the IISReset, I still got the same error ... but a restart of the Timer service goes a long way.

terça-feira, 25 de setembro de 2012

The Ilusive jsGrid (V)

And now something long due: how to display comboboxes/dropdowlists on the jsGrid.

So at a given point in your code you'll have - i hope - something similar to this:

GridSerializer gds = new GridSerializer(SerializeMode.Full,
dataTable,
"key",
new FieldOrderCollection(),
GetGridFields(),
GetGridColumns());

Your GetGridFields() function should resemble this:

protected IList GetGridFields()
{
   List row = new List();
   foreach (DataColumn iterator in dataTable.Columns)
   {
      GridField field = new GridField();
      field = FormatGridField(field, iterator);
      row.Add(field);
   }
   return row;
}

Now let's focus on FormatGridField(gridField, dataColumn). Let's say the column that should display the comboboxes is named "brands". This is what should be included in your FormatGridFields(...) function:
(...)
if (dc.ColumnName == "brands") //our combobox column
{
   gf.PropertyTypeId = "myLookupTable"; //Use whatever name you want here; just be aware that it will be used on the js file where the GridManager is instatiated
   gf.Localizer = (ValueLocalizer)delegate(DataRow row, object toConvert)
   {
      string brandName = (string)row["brandName"];
      return toConvert == null ? "" : brandName;
   };
   gf.SerializeDataValue = true;
   gf.SerializeLocalizedValue = true;
}
(...)

That's all on the server side.

As for the js file where the GridManager is instatiated, you'll have something similar to this:
GridManager = function () {
   this._jsGridControl = null;
   this.Init = function (jsGridControl, initialData, props) {
(...)

Add this inside that same function:
SP.JsGrid.PropertyType.RegisterNewLookupPropType('myLookupTable', brandsArray, SP.JsGrid.DisplayControl.Type.Text, false); //Remember "myLookupTable" from before; it's up to you to get the brandsArray filled with the possible values; i use an hiddenfield, then move the contents into the array.

(a couple of lines below)

jsGridParams.tableViewParams.columns.GetColumnByKey('brands').fnGetEditControlName = function () { return SP.JsGrid.EditControl.Type.ComboBox; } //"brands" is the name of the column
jsGridControl.Init(jsGridParams);

That's all there is to it - (from top to bottom) tell the grid that the EditControl for the column is a ComboBox; also, register the lookup table that contains all the possible values; change the server side code so that the combobox displays previously saved values.

I hope this is useful.

Soundtrack for this finding:
Haven't heard much music lately, but "Bandoliers" by Them Crooked Vultures certainly played a part.

quinta-feira, 3 de maio de 2012

The Ilusive jsGrid (IV)

Here we are again! So, let's say we want to mess around with rows availability: disable this one, enable that one depending on some other cell value...

First stop (not exactly, just sort of): (on GridManager.Init()) jsGridControl.SetDelegate(SP.JsGrid.DelegateType.GetRecordEditMode, myGetRecordEditMode);

This is what myGetRecordEditMode should look like

function myGetRecordEditMode (record) { ... }

Now, let's say our table has a column named 'status', containing integers (that represent different statuses), and that for some statuses (like '0' or 'Draft') the corresponding row should be editable, and for others it should be read-only (like '5' or 'Closed'). It's easy:

function myGetRecordEditMode(record) {
if (record.properties['status'].dataValue == 0) //Draft
return SP.JsGrid.EditMode.ReadWrite;
if (record.properties['status'].dataValue == 5) //Closed
return SP.JsGrid.EditMode.ReadOnly;
return SP.JsGrid.EditMode.ReadOnlyDefer; //All other status
}

The star of the show here is the «SP.JsGrid.EditMode» enum - this is its definition: SP.JsGrid.EditMode={ ReadOnly: 0, ReadWrite: 1, ReadOnlyDefer: 2, ReadWriteDefer: 3, Defer: 4 }; more details here microsoft.sharepoint.jsgrid.editmode (warning: this is the server-side counterpart, but it fits the purpose). It helps making row enabling/disabling quite simple; the real trick - and this is why in the previous code example all other status get the SP.JsGrid.EditMode.ReadOnlyDefer value, and not just SP.JsGrid.EditMode.ReadWrite - is mixing row editability with column editability. I'll provide more details in a future post.

quinta-feira, 15 de março de 2012

AllowUnsafeUpdates + Workflow = Disaster

Having worked on numerous occasions with custom permissions - adding, removing ... - i've used the SPWeb.AllowUnsafeUpdates property extensively to avoid the "security validation exception, go back" thing. Recently, i had the need to once more deal with custom permissions but for the first time within workflows. So, right away i used AllowUnsafeUpdates to prevent getting the "security validation" exception.

I must say i came very close to tear my own scalp off, because despite having used AllowUnsafeUpdates to the best of my ability, i was still getting slapped with that same exception locking workflow instances with "Failed on Start (retrying)" over and over again.

Moral of the story: the AllowUnsafeUpdates is as useful to the SharePoint workflow runtime, like a shotgun in Chuck Norris's hands. So don't use it.

Soundtrack for this finding:
No music here 'though "Highway to Hell" would've been the perfect choice

Required fields and the check in/out mechanism

This probably very, very basic but i only realized it the hard way: having required fields on a document library, causes all new/uploaded files/items to be checked out automatically, even if the "Require Check Out" list setting is not enabled. Aditionally, no workflow instances will start as expected - lock on "Starting" - until those files/items are checked in.

Soundtrack for this finding:
No music here

quinta-feira, 9 de fevereiro de 2012

Strange But True: Reading Timesheets

According to the Project 2010 SDK, there's little difference in using TimeSheet.ReadTimesheetByPeriod - "Gets the timesheet header, line items, and timephased data based on the specified period and resource." - or TimeSheet.ReadTimesheet - "Gets the specified timesheet header, line items, and timephased data." - except the number/type of parameters; the first method takes in the resource ID, period ID and "navigation", the second just the timesheet ID.

The fact is i've been getting a different number of timesheet lines by calling one or the other, being TimeSheet.ReadTimesheetByPeriod the one that returns fewer lines. It can't be an issue with the tasks/assignments because they are visible in the Timesheet web form, and even if that was the case, there would still be no explanation why those lines are returned by TimeSheet.ReadTimesheet.

In any case, here's a simple workaround:
  TimesheetDataSet timesheetSet = timesheetClient.ReadTimesheetByPeriod(resourceUID, periodUID, SvcTimeSheet.Navigation.Current);
  timesheetSet = timesheetClient.ReadTimesheet(timesheetSet.Headers[0].TS_UID);

Soundtrack for this finding:
Megadeth - Skin O'My Teeth, Foreclosure Of A Dream

sábado, 7 de maio de 2011

The Ilusive jsGrid (III)

How to enable/disable editing of the jsGrid's content? For a change, something simple:

jsGridCtrl.EnableEditing() and jsGridCtrl.DisableEditing()

Soundtrack for this finding:
Megadeth - Pray For Blood

terça-feira, 3 de maio de 2011

Adding a new Team Member to an existing Project through PSI 2010

Nothing new here except...

ProjectTeamDataSet.ProjectTeamRow newTeamMember = projectTeamDSet.ProjectTeam.NewProjectTeamRow();
newTeamMember.RES_UID = resourceID;
newTeamMember.NEW_RES_UID = resourceID;
newTeamMember.RES_NAME = SPContext.Current.Web.CurrentUser.Name;
newTeamMember.PROJ_UID = projectID;
projectTeamDSet.ProjectTeam.AddProjectTeamRow(newTeamMember);

If you don't set newTeamMember.NEW_RES_UID, before calling projectSrvClient.QueueUpdateProjectTeam(), you're dead ... (ProjectResourceNotFound)

Soundtrack for this finding:
Mustallica - Jump In The Fire

quinta-feira, 14 de abril de 2011

The Ilusive jsGrid (II)

You've set up your jsGrid "correctly", there's no compilation/deployment/script errors but still it won't render - the only visible element is the "Loading" gif where rows and columns should be...

Check your GridManager (script) object; something must be wrong with the tableViewParams. I found this when i made changes to my (server-side) DataTable structure - removed columns - and failed to reflect those changes in the client script, as i had defined some delegates involving the removed columns. And as i said before, no visible error ...

Soundtrack for this finding:
Prince - Purple Rain (i know, not heavy metal ...)

The Ilusive jsGrid

Go figure: the SharePoint team has created a fantastic control, the jsGrid, and for some reason felt it shouldn't provide the developers with something worth calling "documentation". (Really, what's the point? There are always some chumps who'll do it for them anyway - and for free!)

So, for starters, how to set up a column to contain an image/icon with a tooltip?

First, in the GridManager.Init() method there will be (by default) something like
(...)
var jsGridParams = dataSource.InitJsGridParams();
(...)
Once this is done, the grid's columns can be accessed like this:
jsGridParams.tableViewParams.columns.GetColumnByKey('#columnName#')

Obviously, there has to be a DataColumn named #columnName# in the DataTable used as a source for the jsGrid. It can be a simple string/text column - note: this GridField = String - filled with data in the format "#image url#;#image tooltip#".

Back to the jsGridParams.tableViewParams.columns.GetColumnByKey ...
Two delegates must be defined, like in the example below, in order that the cells display the image and its tooltip.

jsGridParams.tableViewParams.columns.GetColumnByKey('#columnName#').fnGetDisplayControlName = GetImageDisplayControlName;
jsGridParams.tableViewParams.columns.GetColumnByKey('#columnName#').fnGetSingleValueTooltip = GetImageTooltip;

These are the methods signatures
function GetImageDisplayControlName(column, record, fieldKey)
function GetImageTooltip(record, colName, dataValue, localizedValue)

and these are possible implementations

function GetImageDisplayControlName(column, record, fieldKey) {
record.properties[fieldKey].propType.GetImageSource = function (rcrd, p2) {
var valueInDataTable = rcrd.properties[fieldKey].localizedValue;
if (valueInDataTable != null && valueInDataTable.length > 0) {
return #pathToImage#;
}
return '#pathTo#/blank.gif';
};
return 'DISP_IMAGE';
}

The most important point here is the "return 'DISP_IMAGE'"; this is what this delegate is all about: telling the jsGrid how to render a particular cell. But before that, the GetImageSource method must be set up in order for it to return the path of the image to display. Because the cell contains data in the "#image url#;#image tooltip#" form, this is where the split (by ';') is made.

function GetImageTooltip(record, colName, dataValue, localizedValue) {
var valueInDataTable = record.properties[colName].localizedValue;
if (valueInDataTable != null && valueInDataTable.length > 0) {
if (valueInDataTable.indexOf(';') == -1)
return val;
return valueInDataTable.split(';')[1];
}
return '';
}

As for GetImageTooltip, the process is pretty clear and self-explanatory.

Hope this helps.

Soundtrack for these precious findings:
Pantera - Revolution is my name
Megadeth - Rust in peace ... Polaris