Friday, December 23, 2016

Shortcircuit launch sequence of provider-hosted Add-Ins

The launch of provider-hosted Add-Ins consists of 2 recurring steps:
  1. First, go to the SharePoint hostweb, and request AppRedirect.aspx
  2. The returned AppRedirect.aspx response contains a FORM structure, with as Action parameter the start-url of Add-In; via javascript embedded in the AppRedirect.aspx response the browser POST submits to request the Add-In start page.
See Source: Chris Johnson, Launching SharePoint Apps, launch sequence, debugging and AppRedirect.aspx
An essential role and value of AppRedirect.aspx is to pass the authenticated identity of current user as context token from SharePoint WFE to the remote web. However, this only applies for OAuth based, low-trust authentication via Azure Access Control Service (ACS). In case of Server-2-Server, High-Trust authentication, SharePoint host(web) environment does not pass a context token at all. And thus there is no concrete added value nor necessity for the AppRedirect call before the launching of the provider-hosted Add-In.
The most important FORM parameter is SPAppToken. SPAppToken is specific to low-trust configurations. If you are using a high-trust configuration the SPAppToken token is missing. Managing Identity and Context in Low-Trust Hybrid SharePoint 2013 Apps

The recurring invocation of AppRedirect.aspx per Add-In launch has some drawbacks:
  1. it involves an extra http request/response handling; in particular with longer network-distance, this costs noticable latency
  2. AppRedirect.aspx itself can take processing and elapse time. In IIS logs I observed that although not always, multiple times the server-side execution of this requests takes > 0.5 seconds to 1 second or more
  3. with higher load, I've earlier (Load testing SharePoint Add-in (former App) Model) noticed that the AppRedirect.aspx handling eventually becomes the bottleneck within the SharePoint WFEs
In the Add-In launch sequence, there is optimization possible as the AppRedirect.aspx step is avoidable in an High-Trust authentication situation. The added-value is in S2S authentication: zero. All the requested information to launch the Provider-Hosted Add-In is already included by SharePoint in the response of the host page that contains the Add-In(s): the 'src' value of provider-hosted Add-In iFrame element(s) exists of the pattern '<host>/_layouts/15/AppRedirect.aspx' and 'redirect_uri=<encoded uri of the Add-In startpage>'. Thus it is possible to shortcircuit the Add-In launch sequence by aborting the AppRedirect.aspx request, and replace it to instead immediate request as iFrame src the url-decoded 'redirect_uri' value.
Options to shortcircuit AppRedirect are:
  • Server-side: via an HTTP-module, to intercept the HTTP response before it reaches the browser, and for every iFrame element replace the value of the src parameter from AppRedirect.aspx into it's redirect_uri value. Disadvantages of this approach are that it is non-discrimating: all responses returned will be parsed, thus also all those that do not even contain an Add-In structure; it requires custom server-side deployment, something that is nowadays not always allowed as companies aim for cloud-ready / future-proof deployments
  • Client-side: on-the-fly change in browser / javascript-engine context, for every iFrame element replace the value of the src parameter from AppRedirect.aspx into it's redirect_uri querystring value. Disadvantage of this approach is that it is late, the browser typically already has requested the AppRedirect.aspx call. It still benefits to on-the-fly change the frame-src, and as such abort the waiting on AppRedirect.aspx response: in particular with longer network-distance, e.g. in case of a global company, it pays out to not wait for AppRedirect.aspx response to return, but immediate request the launch of the provider-hosted Add-In. And also it shortens in the scenario in which the processing time of AppRedirect.aspx would otherwise be [a] noticable bottleneck.
Snippet of the client-side approach:
<lSharePointWebControls:ScriptBlock runat="server"> function ShortCircuitLauchProviderHostedAppsInWPZone(webpartZone) { var frmRedirectSnippet = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"' + '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' + '<html dir="ltr" lang="en-US">' + '<head><meta http-equiv="Expires" content="0" /><meta http-equiv="X-UA-Compatible" content="IE=8"/><title>Working on it...</title>' + '<link rel="shortcut icon" href="/_layouts/15/images/favicon.ico?rev=23" type="image/vnd.microsoft.icon" />' + '</head>' + '<body>' + '<div style="text-align:center;margin-top:40px;">' + '<img src=\'data:image/gif;base64,R0lGODlh...'>' + '<span style="color:#0072c6;font-size:36px;font-family:\'Segoe UI Light\', \'Segoe UI\', Tahoma, Helvetica, Arial, sans-serif;">' + 'Working on it...' + '</span>' + '</div>' + '</body>' + '</html>'; var iframesProviderHostedApps = webpartZone.querySelectorAll('iframe[src*="appredirect.aspx"]'); var i; for (i = 0; i < iframesProviderHostedApps.length; i++) { var src= iframesProviderHostedApps[i].getAttribute("src"); var redirect_uri = src.substring(src.indexOf("redirect_uri=") + "redirect_uri=".length); var addInUrl = decodeURIComponent(redirect_uri); iframesProviderHostedApps[i].contentWindow.document.write(frmRedirectSnippet); iframesProviderHostedApps[i].setAttribute('src', addInUrl); } } ShortCircuitLauchProviderHostedAppsInWPZone(document.getElementById("app_1")); </SharePointWebControls:ScriptBlock>

The client-script approach enables yet another performance and resource usage optimization: delayed loading per Add-In until and only if it is in the visible part of the browser window. The user experiences the effect one is familair with on mobile devices: swipe the screen, and the application's content is on-the-fly retrieved + screen estate filled (Twitter, Facebook, ...).
Slightly modified snippet for lazy loading Provider-Hosted Add-Ins:
<lSharePointWebControls:ScriptBlock runat="server"> function isWithinVisibleViewport(element) { var $w = jQuery(window); var windowWidth = $w.width(), windowHeight = $w.height(), viewTop = $w.scrollTop(), viewBottom = viewTop + windowHeight, viewLeft = $w.scrollLeft(), viewRight = viewLeft + windowWidth, offset = element.offset(), _top = offset.top, _bottom = _top + element.height(), _left = offset.left, _right = _left + element.width(), compareTop = _bottom, compareBottom = _top, compareLeft = _right, compareRight = _left; return ((compareBottom <= viewBottom) && (compareTop >= viewTop)) && ((compareRight <= viewRight) && (compareLeft >= viewLeft)); } function ShortCircuitLauchProviderHostedApps() { var nonVisibleInViewport = false; var frmRedirectSnippet = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"' + ..... '</html>'; var iframesProviderHostedApps = document.querySelectorAll('iframe[src*="appredirect.aspx"]'); var i; for (i = 0; i < iframesProviderHostedApps.length; i++) { var delayedFrame = iframesProviderHostedApps[i]; var src = jQuery(delayedFrame).attr('src'); if (src != '') { var redirect_uri = src.substring(src.indexOf("redirect_uri=") + "redirect_uri=".length); var addInUrl = decodeURIComponent(redirect_uri); } else { addInUrl = jQuery(delayedFrame).attr('DelayedSrc'); } if (isWithinVisibleViewport(jQuery(delayedFrame))) { jQuery(delayedFrame).contents().find('html').html(frmRedirectSnippet); jQuery(delayedFrame).setAttribute('src', addInUrl); } else { jQuery(delayedFrame).setAttribute('src', ''); jQuery(delayedFrame).setAttribute('DelayedSrc', addInUrl); nonVisibleInViewport = true;' } } jQuery('#s4-workspace').off('scroll', ShortCircuitLauchProviderHostedApps); if (nonVisibleInViewport) { jQuery('#s4-workspace').on('scroll', ShortCircuitLauchProviderHostedApps); } } </SharePointWebControls:ScriptBlock>

Sunday, December 11, 2016

Prerequisites for Office 365 implementation experts

When an organization starts with the implementation of Office 365, there is a need for different roles. Each of them puts its own requirements on people to add value for the Office 365 onboarding plus implementation.
Below an enumeration of expertise that I - on personal notice - consider as relevant to help in making Office 365 introduction succesful.
Office 365 / SharePoint Online project lead
  • Project lead at minimal 1 medium to large organization that successful transitioned to Office 365 / SharePoint Online
  • Familiar with the different stakeholders and their respective stakes/interests in Office 365 implementation project:
    • Business users
    • Security
    • SharePoint Operations
    • Identity Management team
    • Network team
  • Office 365 / SharePoint Governance knowledge + practical experience
  • Able to setup project planning, monitor the team/project progress, and adjust appropriate in case of issues
  • Structured work approach / attitude; yet also goal-oriented / pragmatic
  • Clear communication to different audiences on plans, progress and status of the project
Office 365 security expert
  • Familiar with the full range of Office 365 Security Controls
    • Access Control
    • Information Rights Management: concepts + implementation
    • Incident Response & Recovery
  • Experienced with Office 365 Identity Management + Provisioning aspects
  • Familiar with different possible authentication models (username/password, multi-factor authentication via hard- and soft-tokens), and how-to setup conditional access
Office 365 / SharePoint Online implementation + functional consultant
Understand the service on functional level: how to use the functional building blocks, their positioning, their limitations
  • At minimal 1 year, demonstrable working experience with setting up Office 365 / SharePoint Online sites + functional solutions
  • Familiar with configuration aspects in SharePoint Online
  • Familiar with global changes and deviations of SharePoint features / functions on-prem (preferable 2010) versus Online ‘Branding’
    • Lists and Libraries
    • Workflows
  • Communicate with business to analyze functional needs, and translate how to deliver on these within Office 365 landscape
  • Preferred: hands-on experience within migration from SharePoint on-prem to Online
Office 365 / SharePoint Online technical consultant
  • At minimal 1 year, demonstrable working experience with developing custom business solutions in Office 365 / SharePoint Online
    • Client-side and/or Server-side
  • Working experience with developing SharePoint Add-Ins (on-prem and/or online)
    • Client-side and Server-side
  • Experienced with Office 365 development model
    • Office 365 API
    • Azure Authentication handling (adal.js + OAuth)
    • Azure configuration + deployment
    • PowerShell scripting
  • Preferred: practical experience with the new SharePoint Framework (SPFx)

Friday, December 9, 2016

Freeze pane on SharePoint list with responsive scroll height

Earlier I blogged on how-to 'Freeze pane on SharePoint list': augment the standard SharePoint ListView UI to fixate the list header in visible sight when the user scrolls down the list in the browser. The user was totally happy with this delivered solution..., until opening the page on smaller screen (estate)... In the original code, the browser scrollbar is explicit hidden. An non-usability effect is that the user cannot scroll to the lower part in case the configured height of the 'scrollable listview' extends beyond the physical browser height. On the first coding, I made the explicit decision to hide the browser scrollbar via CSS, to avoid double scrollbars: without the CSS "overflow:hidden" property on 's4-workspace', the browser will always render its own scrollbar for pages that initial extend the browser height, even when later via clientside coding the page height is on-the-fly reduced to fit in browser height. The presence of double scrollbars - browser + on the list - is both confusing as non-esthetic. I prevented this via the 'overflow:hidden'.
However, in the initial code I didn't accompensate sufficient for smaller physical screens. I re-evaluated the 'freeze' code, and made some adjustments to make it responsive to the physical screen height. Another fix is to duplicate eventhandling on the standard listview header to the frozen header, for filtering, and 'select/deselect all' functionalities. Yet another fix is in the positioning of the ECB menu: default this is positioned relative to the listview top, which however itself is floating due the 'freeze' handling.
Updated code:
var visibleTableHeight = -1; // Freeze the header of listview. function FreezePane() { var $table = $(".ms-listviewtable").first().data("summary", "<listview name>"); var freezedTRHeight = 25; // Determine the available height for table to render in visible screen. if (visibleTableHeight === -1) { var origWorkspaceHeight = $("#s4-workspace").height(); var origTableHeight = $table.height(); var spaceInWorkspaceWithoutTable = origWorkspaceHeight - origTableHeight; var windowHeight = window.innerHeight; if (windowHeight == undefined) { windowHeight = document.documentElement.clientHeight; } var topPos = $("#s4-workspace").offset().top; var availableHeight = windowHeight - topPos - spaceInWorkspaceWithoutTable - freezedTRHeight; // Set visible height, with minimum of 300 visibleTableHeight = Math.max(availableHeight, 300); } // WRAP TABLE IN SCROLL PANE $("#s4-workspace").css( { 'overflow-y': 'auto' } ); $table.wrap("
"); // FROZEN HEADER ROW $(".FreezedLV").wrap("<DIV class='FreezedLVContainer' style='position:relative; margin:0px; height:" + (visibleTableHeight + freezedTRHeight) + "px;'></DIV>"); $("<table id='FreezedTR' class='ms-listviewtable' cellPadding='1' cellSpacing='0'></table>").insertBefore(".FreezedLV"); $("#FreezedTR").width($table.width() + "px"); var $origHeader = $("TR.ms-viewheadertr:first", $table); var $firstRowTable = $("TR.ms-itmhover:first", $table); var $freezeHeader = $origHeader.clone(); // Propagate computed width of columns of origheader to freezeheader $origHeader.children("th").each(function() { var width = $(this).width(); var ownerIndex = $(this).index(); $($freezeHeader.children("th")[ownerIndex]).width(width + "px"); }); var $alignerRow = $firstRowTable.clone(); $alignerRow.css( { 'visibility' : 'hidden' } ); $("#FreezedTR").append($alignerRow); $("#FreezedTR").wrap("<DIV style='HEIGHT: " + freezedTRHeight + "px;'></DIV>"); // Visual "hide" the orig header, make sure it still is rendered as otherwise the alignment of 'body' rows is altered $origHeader.children("th").each(function() { $(this).css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px' , 'padding-top' : '0px', 'padding-bottom' : '0px' } ); $(this).find("div").css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px', 'margin-top' : '0px', 'margin-bottom' : '0px' } ); $(this).find("input").css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px' } ); }); $origHeader.css( { 'max-height' : '1px', 'visibility' : 'hidden' } ); // Delegate eventhandlers from the copied+freezed header to the actual header of the listview. var $inputFH = $freezeHeader.find("input[title='Select or deselect all items']"); $inputFH[0].onclick = null; $inputFH[0].onfocus = null; $table.onmouseover = null; $inputFH.click(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-viewheadertr").find("input[title='Select or deselect all items']").trigger('click'); }); $inputFH.focus(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-viewheadertr").find("input[title='Select or deselect all items']").trigger('focus'); }); $("#FreezedTR").mouseover(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-listviewtable").trigger('mouseover'); }); ExecuteOrDelayUntilScriptLoaded(OverloadPositionCtxImg, "core.js"); Overload__doPostBack(); } // Need to update/overload positioning of 'ECB' icon as SharePoint standard it is relative positioned towards its direct parent; // and due scrolling would become invisible. function OverloadPositionCtxImg() { if ($.prototype.KI_base_PositionCtxImg === undefined) { $.prototype.KI_base_PositionCtxImg = PositionCtxImg; PositionCtxImg = function(c, b, h) { $.prototype.KI_base_PositionCtxImg(c, b, h); var a = c.style; a.top = (c.parentNode.offsetTop + b.clientTop) + "px"; }; } } // Overload MicrosoftAjax UpdatePanel function, to restore FreezePane after navigating to another page in the overview. function Overload_updatePanel() { if (Sys.WebForms.PageRequestManager.prototype.KI_base__updatePanel === undefined) { Sys.WebForms.PageRequestManager.prototype.KI_base__updatePanel = Sys.WebForms.PageRequestManager.prototype._updatePanel; Sys.WebForms.PageRequestManager.prototype._updatePanel = function(updatePanelElement, rendering) { Sys.WebForms.PageRequestManager.prototype.KI_base__updatePanel(updatePanelElement, rendering); if (updatePanelElement === $(".ms-listviewtable").first().data("summary", "<listview name>").closest("div")[0]) { if (("td.ms-addnew").length > 1) $("td.ms-addnew").last().closest("table").hide() FreezePane(); } }; } } function Overload__doPostBack() { if ($.prototype.KI_base__doPostBack === undefined) { $.prototype.KI_base__doPostBack = __doPostBack; __doPostBack = function (eventTarget, eventArgument) { // Make sure to overload the updatePanel function before postback from navigate button. if ($.prototype.KI_base__updatePanel === undefined) Overload_updatePanel(); $.prototype.KI_base__doPostBack(eventTarget, eventArgument); } } }

Sunday, November 27, 2016

Large time-taken value in IISlogs do not per se mean an issue on IIS / SharePoint server level

The time-taken field measures the length of time that it takes for a request to be processed. The client-request time stamp is initialized when HTTP.sys receives the first byte of the request. HTTP.sys is the kernel-mode component that is responsible for HTTP logging for IIS activity. The client-request time stamp is initialized before HTTP.sys begins parsing the request. The client-request time stamp is stopped when the last IIS response send completion occurs.
In the IIS logs of our SharePoint WFEs I observed large time-taken values for the responses of several GET requests, with times above 2500 (!!) seconds.
LogParser.exe "SELECT TOP 10 date,time,c-ip,cs-uri-stem,sc-status,sc-substatus,time-taken from u_ex*.log WHERE time-taken > 2500000 ORDER BY time-taken DESC"
We investigated whether this points to an actual performance problem on SharePoint server side, or is merely to be considered a false positive wrt the performance of the SharePoint environment. An indication of the latter is that our end-users have not reported (complained) on reoccuring long wait times for these requests. After some lengthy investigation - the observed symptom is not well documented -, we conclude to the last.
The justification for this conclusion is a) that we did not identify any performance issue due these specific requests on the SharePoint WFEs, and b) that beginning in IIS 6.0, the time-taken field typically includes “all the network time” being spent when transferring all the bytes to/from client:
HTTP.sys waits for the client to acknowledge the last response packet send operation or HTTP.sys waits for the client to reset the underlying TCP connection, before it logs the value for process duration in the time-taken field in the IIS logs.
This correlates with our observation that the longer time-taken responses were all for the download of large static files, typical powerpoint files (.pptx), that are rendered in browser via Office Web Apps (OWA). It appears that the browser on occassion takes long time before it completely processed the received file response, and acknowledges the last packet.

Wednesday, November 16, 2016

Recipe to duplicate InfoPath ListForm template for another content type

Context:
  • Crafted a rather rich and extensive InfoPath ListForm for content type <X>
  • Business requests similar but slightly different form for content type <Y>
Difficulties
  • InfoPath Designer does not provide a native capability to duplicate an InfoPath form to another content type
  • Drag-and-Drop from source InfoPath form template opened in InfoPath Designer to destination form template is limited, incomplete layout, no controls binding, ...
Pragmatic recipe for InfoPath Form copy
  1. Navigate to SharePoint List in a browser
  2. Go to 'List settings' \ 'Form Settings'
  3. Select content type <X>, the associated form opens in InfoPath Designer
  4. Save the template.xsn on a local location (or 'Save as' of the "source files")
  5. Return to 'List settings' \ 'Form Settings'
  6. Select content type <Y>, and select to create new form in InfoPath
  7. Immediate save as template.xsn on a local location, but make sure not to overwrite the one of <X>
  8. Navigate in Windows Explorer to the location(s) where you saved the .xsn files
  9. Rename the .xsn file-extension into .cab
  10. Extract both template.cab archives to local folders
  11. Open the folder of template for <Y>, and open template.xsf file in an editor. Lookup 'ContentTypeID' in the file, and copy the value
  12. Open the folder of template for <X>, and open template.xsf file in an editor. Lookup 'ContentTypeID' in the file, and paste the value copied from <Y>. Save the file.
  13. Make sure to reassign 'Open with' file association for template.xsf file to InfoPath Designer
  14. Double click the modified template.xsf file, and click 'Design' to open in InfoPath Designer
  15. Publish the InfoPath form to SharePoint List
Pragmatic recipe for partial InfoPath Form copy/reuse
  1. Step 1-10 as above
  2. Open the folder of template for <Y>, open in an editor the view.xsl for which you want to reuse layout from the form template of content type <X;>, and delete all content
  3. Open the folder of template for <X>, open in an editor the view.xsl that you want to reuse, select all file content for copy
  4. Paste the copied file content in the opened view.xsl of content type <Y>, and save the file
  5. Repeat this for every view in the form template that you want to duplicate/reuse
  6. Double click the modified template.xsf file, and click 'Design' to open in InfoPath Designer
  7. Publish the InfoPath form to SharePoint List

Thursday, October 13, 2016

Web-distribution of plain-old HTML/JS/CSS solution in Office 365

One of the many capabilities of SharePoint is that of webserver platform: host and distribute HTML/JavaScript/CSS based (mini)applications. With the birth of the SharePoint Framework (SPFx), I expect this role to be utilized a lot more in near future. The typical SharePoint setup for this web-distribution role is 1) a SharePoint page, 2) a ScriptEditor (or good-old ContentEditor webpart), and 3) the (mini)application itself consisting of an html-file for UI, potential javascript and/or css file(s), all stored as (SharePoint) content entities in a document library, or in a CDN.
SharePoint Online / Office 365 also supports this setup, and thus should be an equal fit for the role of web-distribution. However, you need to be aware of 3 aspects:
  1. Default, custom script is not allowed in Office 365 tenant.

    You'll notice the effect of this when you try to add custom script in a ScriptEditor, or on using PageViewer webpart which refuses to remember the url to the (mini)application html file - (Office 365 SharePoint Online - Page Viewer Web Part Not Working)

    The resolution is to set in SharePoint Online Admin settings the allowance for custom script - (Turn scripting capabilities on or off). Be aware that it can take up to 24 hours before the effect of the change is actually applied in your Office 365 tenant.

  2. ContentEditor prohibits 'FORM'tags in content:

    The resolution is to indeed use a PageViewer webpart, that is if the HTML contains a FORM tag. If not, good old CEWP accept the HTML as included content.

  3. SharePoint Online only supports download action on .htm(l) file

    The Microsoft SharePoint team made an explicit design decision to only allow download of html files, and no rendering in browser - (Office 365 - Open html document in browser in document library).

    The simple resolution is to change the extension of the .htm(l) files into .axpx, then SharePoint (Online) will no longer include the "X-Download-Options: noopen" header in the HTTP response, and the file is opened in the browser rendering the html code.

Friday, October 7, 2016

On-the-fly extend ECB-menu

Say you have business data administrated in a SharePoint list/library, and you want to enable a custom action on the items. In the "old days" one would create a Feature that deploys a CustomAction. But nowadays we intend to / or even must (Office 365 /SharePoint Online) avoid server-side (or sandbox) deployments. Clientside-development is the new mantra. SharePoint as webapplication is strongly javascript-based, from 2007 and beyond. Also the handling of the Edit-Control-Block menu is via SharePoint javascript code. This makes it possible to "break" into that execution, and inject some own handling. Via F12 DeveloperTools I identified where to 'break into'/extend:
var EnrichListView = window.EnrichListView || {}; EnrichListView.UI = function () { function MyCustomItemAction(itemId) { // do something with the item identified by 'itemId' } function OverloadMenuHtc_show() { $.prototype.base_MenuHtc_show = MenuHtc_show; MenuHtc_show = function(oMaster, oParent, fForceRefresh, fFlipTop, yOffset) { if ($(oMaster).find("SPAN[id='ID_EditItem']").length == 1) { var itemId = oParent.childNodes[0].id; var spanInsert = $("<SPAN text='Custom Action on item' onMenuClick='EnrichListView.UI.MyCustomItemAction(" + itemId + ")' type='option' iconAltText sequence='200' CUICommand='CustomActionItems' ></SPAN>"); $(oMaster).append(spanInsert); } $.prototype.base_MenuHtc_show( oMaster, oParent, fForceRefresh, fFlipTop, yOffset); return false; }; } var ModuleInit = (function() { ExecuteOrDelayUntilScriptLoaded(OverloadMenuHtc_show, "core.js"); })(); // Public interface return { MyCustomItemAction: MyCustomItemAction } }();
Result:
This client-side approach is on itself future-proof, and also works online. The only caveat is that online Microsoft may change the javascript handling/code on which this code breaks into, without you knowing. The javascript code can also change due an update to SharePoint on-premise installation, but then you're aware of a change in the SharePoint installation and can prepare for the change in advance.

Tuesday, October 4, 2016

Automatic revert-to-self for InfoPath forms

(Functional) users working with more complex InfoPath forms may be familiar with below notorious error dialog:
A typical cause is that after saving the listitem, in the SharePoint background one or more workflows are triggered that also update the listitem. And that makes the state as loaded in the form (stateless setup) outdated, and InfoPath refuses next save due optimistic locking. Pragmatic way out is to force data reload in the form, and most simple approach to that is close the form as last action in the ‘Save’ rule. Drawback is that the user must self relocate the listitem again, reopen and set into edit modus.
Via javascript injection the standard InfoPath Forms Services handling can be overruled, and instead automate the relocate/reload/edit - revertToSelf:
var InfoPathCheck; var maxNrChecks = 10; $(document).ready(function(){ InfoPathCheck = setInterval(function() { //Wait for InfoPath to finish loading HasInfoPathLoaded(); }, 200); }); function HasInfoPathLoaded() { var selectAppl = $("#<form-id>"); if (selectAppl.length > 0 || --maxNrChecks === 0) { clearInterval(InfoPathCheck); if (selectAppl.length > 0) { var saveBtns = $("input[id^='<form-id>_FormControl'][value^='Save']"); if (saveBtns.length > 0) { $(saveBtns).click(function() { // Overload IP-function to return-to-self upon 'Save'. CurrentFormData_UrlToNavigateToOnClose = function(a) { return document.location.toString(); } }); } } } }

Friday, September 30, 2016

Hide columns from (grouped) ListView with conditional formatting

Calculated Fields in SharePoint Lists enable conditional (rich) formatting of raw data from one or more other columns in the same List. This is typically used to ‘business-beautify’ the data display for the end-users. Example is to render ‘Green/Orange/Red’ headlights for immediate visual reference, based on the raw ‘text’ data of a status/progress field. In such usage setup, only the conditional columns should be visible for end-users, not the raw data columns. However, the execution of SharePoint ListView requires that the source data columns are included in the view, otherwise the conditional columns lack their source data in the listview and render empty (or with an error, dependent upon formula).
Hide the columns from display
There are multiple approaches to still hide the ‘raw data columns’ from end-user visibility. My preference is for a CSS-based approach: hide via nth-child selector. For SharePoint 2010 + IE an extension is needed on top of this: IE only supports nth-child as of IE9, but the standard DocumentMode for SharePoint 2010 is IE8 (multiple of the standard SharePoint 2010 functionalities require this, and give browser-problems with document mode later than IE8). Luckily via jQuery the nth-selector is also available for the older IE-versions. The full setup to hide columns in a listview is therefore 2-staged:
  1. CSS:
    /* * Below CSS-selector is supported in >= IE9, Chrome, FF; but not supported in IE8 */ .ms-listviewtable th:nth-child(3), … .ms-listviewtable td:nth-child(3), … { display:none; } /* * This to compensate with non-support of nth-child in IE8 (compatibility mode) */ .ms-listviewtable th.Hide, .ms-listviewtable td.Hide { display:none; }
  2. JavaScript:
    function HideRawDataColumns() { $(‘.ms-listviewtable th:nth-child(3)’).addClass(‘Hide’); $(‘.ms-listviewtable td:nth-child(3)’).addClass(‘Hide’); …. } $(document).ready(function () { HideRawDataColumns (); });
Grouped Views
Above approach breaks for ListViews with one or more Group By applied. The cause is in the deferred loading behavior by the ListView rendering: the data is delayed loaded only when the user clicks to open a group header, and also delayed rendered. For the fully CSS-based approach, this is not an issue: the CSS will be applied for all elements, also when later added to the rendering. The formula is a bit different though, as you need to distinguish between the ‘Grouping’ rows and the ‘Data’ rows. However, for SharePoint2010 + IE it is different. On initial page load moment, the data rows might not be present yet in the DOM, and the IE8 / JavaScript approach to explicitly determine + tag the TD-childs will fail. Solution is to delay the CSS-tagging to after the moment that the data-rows are actually added to the DOM. This requires to hook into the standard ListView javascript handling.
  1. CSS:
    /* * Below CSS-selector is supported in >= IE9, Chrome, FF; but not supported in IE8 */ .ms-listviewtable th:nth-child(3), … .ms-listviewtable td:nth-child(3), .. .ms-listviewtable tbody:not([groupString]) td:nth-child(3), … { display:none; } /* * This to compensate with non-support of nth-child in IE8 (compatibility mode) */ .ms-listviewtable th.Hide, .ms-listviewtable td.Hide { display:none; }
  2. JavaScript:
    var HideFormatColumnsListView = window.HideFormatColumnsListView || {}; HideFormatColumnsListView.UI = function () { function HideRawDataColumns() { $(‘.ms-listviewtable th:nth-child(3)’).addClass(‘Hide’); $(‘.ms-listviewtable td:nth-child(3)’).addClass(‘Hide’); …. if ($.prototype.base_UpdateCtxLastSelectableRow === undefined) { $.prototype.base_UpdateCtxLastSelectableRow = UpdateCtxLastSelectableRow ; UpdateCtxLastSelectableRow = function(clvpCtx, clvpTab) { $.prototype.base_UpdateCtxLastSelectableRow(clvpCtx, clvpTab); var groupRow = clvpCtx.clvp.tBody; if (groupRow !== null && groupRow !== undefined) { $('td:nth-child(3)', $(groupRow)).addClass('Hide'); … } }; } } var ModuleInit = (function() { ExecuteOrDelayUntilScriptLoaded(function () { HideRawDataColumns() }, "inplview.js"); })(); }();

Wednesday, August 10, 2016

SharePoint ignores MIMEtypes mapping unless on web server level

Earlier I reported that our business users request user-intuitive handling of non-Microsoft and non-standard file-formats that are administrated within document libraries. Enabling such in SharePoint on premise consists of 2 steps (See ‘SharePoint: Browser File Handling Deep Dive’ for an authoritative reference):
  1. Add in IIS Manager a MIMEtype mapping for the involved file-extension(s)
  2. Add the MIMEtype(s) to the AllowedInlineDownloadedMimeTypes configuration of the SharePoint webapplication(s)
These server-level configuration steps are not possible in Office 365 / SharePoint Online. I ensured that we can deliver our business users sustainable custom filetype handling on migrating to cloud deployment, via the utilization of Office 365 FileHandler Add-Ins concept.
Wrt MIMEtype mappings, IIS allows multiple levels on which you can configure these:
  • Web server
  • Site
  • Application
  • Physical and virtual directories
  • File (URL)
Note that not all these levels are applicable to SharePoint as virtual webserver.
The web server level is most easy to execute, but drawback is that it requires an IISReset to become active. And that means a temporary down of any SharePoint webapplication hosted on the WFE. Therefore I instructed our operations to add the MIMEtype mappings on the Site level. To be disappointed afterwards: on download of the files from SharePoint, the content-type was still ‘application/octet-stream’ aka the “unknown type”, instead of the configured MIMEtype e.g. ‘application/vnd.spotfire.dxp’. Only when we add the MIMEtype to web server level, SharePoint returns in the file download response the configured MIMEtype.
I conducted an internet search on whether this is intentional, or at least known SharePoint / MIMEtype (mis)behavior. Only found some discussion posts ([1], [2]) inquiring about the effect we noticed (on SharePoint 2010 and SharePoint 2013 Server farms).

Monday, August 1, 2016

Augment the Org Chart with additional information

The SharePoint Organization Browser (Org Chart) is a convenient out-of-the-box functionality to display the organogram on user basis. The information of this typically comes from the HR system, and is propagated to SharePoint User Profiles. In our company we work with a lot of externals. All of them are also administrated in our HR system, and thus also provisioned to the User Profiles. Which is what we want. Only thing that our managers miss, is that in the Org Chart no visual distinction is made between internal and external subordinates. We have this information available in the User Profile in a custom property. But the Org Chart only displays the full names of the colleagues visualized in the organogram chart.
In the ‘old days’ we simple would have built a custom Org Chart, using server-side code. But in the current times, this is strongly advised against. We have an architecture guiding principle that all application customizations and extensions must be future-proof. Modifying or rebuilding something that a platform delivers out-of-the-box does not qualify for us as future-proof. The platform vendor might replace the current component by a complete different setup in the future, of which we would then miss out. In this particular scenario there is another reason to not replace the out-of-the-box delivered by a custom developed. The Organization Browser webpart is standard included on user profile page Person.aspx. To change that we would need to replace the inclusion of the webpart by an own. Note that for Person.aspx publishing page (= SharePoint content) it is possible to do this, on premise and also online, e.g. via SharePoint Designer. However, what online is not possible is to deploy a server-side webpart. Implication is that the future-proof custom Org Chart must be redeveloped fully from scratch (no reuse / extension possible of the standard Org Chart webpart), either via 100% clientside script, or as (Azure deployable) provider-hosted Add-In.
As said, Person.aspx can be modified in both on prem as online. Better sustainable approach is therefore to extend on the out-of-the-box functionality with the requested customization. Inject a reference to an own JavaScript file, and in that file include a method to extend the displayed names of a colleague’s subordinates with information on whether internal or external. We have this information as user profile property, and this can in clientside context be retrieved via the UserProfile REST service.
Dynamic extend the Organisation Browser rendering with additional User Profile information
enrichOrgChartWithExternalOrg = function() { jQuery("#ReportingHierarchy").find("TR.ms-orgme").next().find("TD.ms-orgname > a").each(function(index) { var anchor = this; var href=jQuery(anchor).attr("href"); var accountName = unescape(href.substr(href.indexOf("accountname=") + "accountname=".length)); jQuery.ajax({ url: _spPageContextInfo.webAbsoluteUrl + "/_api/SP.UserProfiles.PeopleManager/GetUserProfilePropertyFor(accountName=@v,propertyName='ExternalOrganization')?@v='" + accountName + "'", contentType: "application/json;odata=verbose", headers: { "accept": "application/json;odata=verbose" }, success:function(data){ if (data.d.GetUserProfilePropertyFor.length != 0) { var extCompName = data.d.GetUserProfilePropertyFor.substr(0, data.d.GetUserProfilePropertyFor.indexOf(" - ")); var augmentedSubordinateName = jQuery(anchor).html() + " (" + extCompName + ")"; jQuery(anchor).html(augmentedSubordinateName); } } }); }); };
Standard Organisation Browser display extended with additional User Profile information
Note, the above code retrieves the extra user profile property per subordinate in a loop-control, each iteration includes a request-reply to the UserProfile REST service. To reduce the network latency impact from the client to SharePoint and back, one would want to avoid the loop and request all in one http request. The REST protocol supports this via $batch (Get UserProfile Properties with REST API Batching. And SharePoint Online and SharePoint 2016 are a good REST citizens, but SharePoint 2013 on premise is not: $batch is not supported. (Make batch requests with the REST APIs).

Friday, July 22, 2016

(scroll)height of children influences scrollheight of page

This is a follow-up on earlier post ‘Freeze pane on SharePoint list
Business users very much appreciate the inserted functionality to freeze the listview header in visible top of screen, while one scrolls through the content. However, they reported a problem in case of smaller display size: it was not possible to scroll completely to the bottom, and also the page navigation controls cannot be reached.
The cause is that in my initial code I decided to unconditional hide the page scrollbar. I decided for this as otherwise the page scrollbar would remain in state to scroll through a large list, despite my changes to reduce the height of that list and give it a scrollbar for itself.
The simple way out to resolve the reported issue would be to just not hide the page scrollbar. But from User Experience point of view I dislike that, the result with 2 scrollbars and the page scrollbar over full initial height, is confusing for end users. I instead opted for only displaying the scrollbar when needed (screen smaller as minimal height for listview), and in such case reduce the scrollheight of page to only scroll to the bottom of the height-reduced listview. Thus not for the full initial height.
At first (html) code attempts this was not that simple to achieve, whatever reduction I made still the page scrollheight remained at the initial large value. Via debugger I identified the cause. The scrollheight of the listview remained at the large value, despite that I reduced the visible height. And the scrollheight of page as parent is directly influenced by the sum of scrollheights of the child elements in page. The scrollheight value is readonly, thus not possible to also reduce this to match the reduced height of the listview Div element.
Post ‘Making elements not affect page height (thus scrolling)’ put me on track to break out of this: It appears that scrollheight of relative positioned children has impact; but not that of absolute positioned children. So what I needed to change in my initial FreezePane code was to break the relative parent-child positioning relation for the listview with respect to page. This is accomplished by following changes:
// Set visible height, with minimum of 300 var visibleTableHeight = Math.max(availableHeight, 300); // $table.wrap("<DIV class='FreezedLV' style='OVERFLOW: auto; HEIGHT: 500px;'></DIV>"); $table.wrap("<DIV class='FreezedLV' style='position:absolute; left:0; top: 0; width:100%; OVERFLOW: auto; HEIGHT: " + visibleTableHeight + "px;'></DIV>"); // $(".FreezedLV").wrap("<DIV class='FreezedLVContainer'></DIV>"); $(".FreezedLV").wrap("<DIV class='FreezedLVContainer' style='position:relative; margin:0px; height:" + (visibleTableHeight + freezedTRHeight) + "px;'></DIV>");
The end-result: now truly happy users, some of which forced to view the page on small(er) laptop screensizes.
The complete 'FreezePane' method:
function FreezePane() { var $table = $(".ms-listviewtable").first().data("summary", "list name"); // Determine the available height for table to render in visible screen. var origWorkspaceHeight = $("#s4-workspace").height(); var origTableHeight = $table.height(); var spaceInWorkspaceWithoutTable = origWorkspaceHeight - origTableHeight; var windowHeight = window.innerHeight; if (windowHeight == undefined) { windowHeight = document.documentElement.clientHeight; } var topPos = $("#s4-workspace").offset().top; var freezedTRHeight = 25; var availableHeight = windowHeight - topPos - spaceInWorkspaceWithoutTable - freezedTRHeight; // Set visible height, with minimum of 300 var visibleTableHeight = Math.max(availableHeight, 300); // WRAP TABLE IN SCROLL PANE $("#s4-workspace").css( { 'overflow-y': 'auto' } ); $table.wrap("<DIV class='FreezedLV' style='position:absolute; left:0; top: 20; width:100%; OVERFLOW: auto; HEIGHT: " + visibleTableHeight + "px;'></DIV>"); // FROZEN HEADER ROW $(".FreezedLV").wrap("<DIV class='FreezedLVContainer' style='position:relative; margin:0px; height:" + (visibleTableHeight + freezedTRHeight) + "px;'></DIV>"); $("<table id='FreezedTR' class='ms-listviewtable' cellPadding='1' cellSpacing='0'></table>").insertBefore(".FreezedLV"); $("#FreezedTR").width($table.width() + "px"); var $origHeader = $("TR.ms-viewheadertr:first", $table); var $firstRowTable = $("TR.ms-itmhover:first", $table); var $freezeHeader = $origHeader.clone(); // Propagate computed width of columns of origheader to freezeheader $origHeader.children("th").each(function() { var width = $(this).width(); var ownerIndex = $(this).index(); $($freezeHeader.children("th")[ownerIndex]).width(width + "px"); }); $("#FreezedTR").append($freezeHeader); $("#FreezedTR").append($firstRowTable.clone()); $("#FreezedTR").wrap("<DIV style='OVERFLOW: hidden; HEIGHT: " + freezedTRHeight + "px;'></DIV>"); // Visualize "hide" the orig header, make sure it still is rendered as otherwise the alignment of 'body' rows is altered $origHeader.children("th").each(function() { $(this).css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px' , 'padding-top' : '0px', 'padding-bottom' : '0px' } ); $(this).find("div").css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px', 'margin-top' : '0px', 'margin-bottom' : '0px' } ); $(this).find("input").css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px' } ); }); $origHeader.css( { 'max-height' : '1px', 'visibility' : 'hidden' } ); // Delegate eventhandlers from the copied+freezed header to the actual header of the listview. var $inputFH = $freezeHeader.find("input[title='Select or deselect all items']"); $inputFH[0].onclick = null; $inputFH[0].onfocus = null; $table.onmouseover = null; $inputFH.click(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-viewheadertr").find("input[title='Select or deselect all items']").trigger('click'); }); $inputFH.focus(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-viewheadertr").find("input[title='Select or deselect all items']").trigger('focus'); }); $("#FreezedTR").mouseover(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-listviewtable").trigger('mouseover'); }); }

Thursday, July 14, 2016

Future-proof handling of custom filetypes in SharePoint document libraries

Build Office 365 FileHandler to return external filetypes with application specific MIME-type(s)

Business in our company works with programs that store data in non-Microsoft and non-standards file-formats, e.g. Mathlab files, MindManager, Spotfire reports, ... Their aim is to store these file-formats also on SharePoint. Two SharePoint issues are an obstacle here.

SharePoint blocks file-extensions for upload

SharePoint blocks a large number of file-extensions for document library upload: Types of files that cannot be added to a list or library. However, in SharePoint 2016 this list is severely reduced to only few .Net programming files left, and in SharePoint Online the file-extension blocking is even completely gone. We’re not yet on SharePoint 2016 nor Online, but our roadmap is heading towards this. Therefore it is viable to relax the blocking to the minimal list of SharePoint 2016.

No recognition of application-specific MIME-type

SharePoint storage is agnostic for file-contents and extension. It can store whatever binary content in the content database as file. However, upon retrieving + opening a file from SharePoint document library, SharePoint has no recollection what to do with the unknown filetypes, and does a best guess. This results that e.g. a Tibco Spotfire report file with extension .dxp, is returned by SharePoint as a .zip file. And next cannot seamless be opened in the Spotfire Player. Same for Mathlab files, MindManager / MindMab, … In SharePoint on-prem you can overload this behavior via configuration of custom MIME-types. However, this is a WebApplication plus IIS setting, and thus clearly not available for usage in SharePoint Online tenant context. This withholds us now from utilizing the on-prem approach, as otherwise we would introduce business disruption upon going to the cloud.
Yet, recently I came across the concept of Office 365 FileHandlers and this promised to be a cloud-ready manner to achieve the same effect. I conducted a proof-of-concept with positive outcome: via a custom build Office 365 FileHandler Add-In that connects to the SharePoint Online site via the Office 365 Graph API, is deployed in Azure and registered in Azure AD, it is possible to open and return non-standard filetypes with custom MIME-types. The FileHandler Add-In concept allows to register multiple handlers, e.g. per specific filetype; and also to have a single FileHandler Add-In that can handle multiple filetypes. For my PoC, I needed to verify that custom MIME-types can be returned in the HTTP-response, and I wanted to validate this for multiple file-types. I therefore restrained to one, and determine in the FileHandler on basis if the extension-part of the filetype, the MIME-type to include in the response. Only disadvantage of this is that the FileHandler administration only allows 1 file-icon association, thus all file-types for which the FileHandler is registrated are displayed in Office 365 document libraries with the same icon.
Office 365 FileHandler applied to SharePoint Online site:
Click to select/download Spotfire file with .dxp extension, result:
and open on client in Spotfire Player application:
Code snippet FileHandler MVC open action to return file-content with application-specific content-type:

ToDo, figure out WOPI protocol against SharePoint Online

SharePoint Online acts as a Web Application Open Platform Interface (WOPI) host, and interacts via the WOPI protocol. A result is that in Office 365 FileHandler activation-parameters the file is identified by WOPI 'FileId' parameter. In my 'generic' FileHandler I want to return the application-specific MIME-type per configured file-extension (remember: I have the single FileHandler Add-In associated with multiple file-extensions), and therefore need the filename with the extension. The WOPI protocol supports this via 'CheckFileInfo' action. So far, I did not manage to get this working, still run into errors on illegal REST requests wrt WOPI protocol. To be continued...

Wednesday, June 29, 2016

SharePoint Hosted Add-In or Plain Old JavaScript / HTML5 / CSS?

When Microsoft released the SharePoint App-model (meanwhile renamed to Add-In model), it almost instantaneously became the preferred and dominant development approach for SharePoint customization. Now several years later, and the hype is gone, the question arises whether the Add-In model is the right answer for all SharePoint customization scenarios.
I dare to say no. And feel myself strengthened in that opinion by Microsoft’s latest and greatest SharePoint development approach: the SharePoint Framework. Yes, the Add-In model brings a lot of value, but at the expense of additional development, maintenance and runtime (performance) costs. In case you can bring the same functional value via a strict JavaScript + HTML5 + CSS setup, you can avoid these additional costs. Simple deploy the functionality via resource files, upload to a document library, and include them in the runtime SharePoint page context via a ScriptEditor. Much simpler to develop and deploy than a SharePoint hosted Add-In. And not hindered by iFrame boundaries.
Does this mean I see no value at all for the SharePoint-hosted Add-In model? No, there are definitely scenarios. A clear one is that of a software vendor: with an Add-In the vendor has a self-contained package that buyers can install in their SharePoint site. Another example is for a piece of functionality that is reused throughout the site structure, and that needs per usage its own storage in SharePoint. The Add-In model can provision this per Add-In installation in either the hostweb or the appweb.

Wednesday, June 8, 2016

Preserve code blocks ('xd:preserve') not supported in InfoPath Forms Services

See Readonly display of MultiLookup value in InfoPath Forms Services: my initial approach was to handle this in the InfoPath template itself, via Custom XSLT:
That approach works when testing in InfoPath Designer - preview.
However, upon publishing the template to SharePoint my custom XSLT is lost. Microsoft in MSDN article Using Custom XSLT in InfoPath Form Templates states that you can protect your XSLT customization for overwriting via 'xd:preserve'. Indeed with that inserted, upon saving the template the customizations are preserved:
However, I ran into another issue that Microsoft does not make clear in the referred MSDN article: forms with preserve code blocks in it cannot be published to SharePoint.
Hidden in other MSDN article Creating InfoPath Form Templates That Work With InfoPath Forms Services it states that 'XSLT extensibility (xd:preserve blocks)' are amongst the 'Features with No Direct Parallel on the InfoPath Forms Services'.
Note that you can [still] apply Custom XSLT in InfoPath Forms Services context, the limitation is that code preservation is not supported. So on every save in InfoPath Designer, the custom XSLT code blocks get overwritten, and you would have to insert them again. That effectively makes this InfoPath customization approach [at least for me] unmanageable and unworkable for sustainable utilization in InfoPath Forms Services context. Therefore my design decision to implement the required behavior 'outside' InfoPath Forms Services, via a clientscript approach.

Readonly display of MultiLookup value in InfoPath Forms Services

Business question:

Have an InfoPath form that on one tab allows submitter to select zero to more impacted IT applications, and on another tab (for other business role) display this selection readonly.

InfoPath / IT answer:

  • Have multiple Views in the InfoPath form / template;
  • On the 'input' View, include a Multiple Selection List (MultiSelectList) control, and bind to the MultiLookup List Column (or ContentType Field);
  • On the 'display' View, visualize the current value of MultiLookup via a Repeating Table control. Via InfoPath configuration (control properties) remove the option for user to include new entries in the table: This part takes care of displaying [only] the selected value(s);
  • What then remains is to make the control readonly, to prevent that the MultiLookup value can be changed on this tab. InfoPath does not itself support to disable the Repeating Table control. However, you can achieve this for InfoPath Forms Services context via (the power of) javascript: include on 'editifs.aspx' page script with a method that sets the html control to disabled. A complexity here is that IFS loads the form asynchronously after the surrounding page is loaded in browser, and that no event is triggered to notify the form is loaded. To handle that, I've programmed a polling approach that recurring checks whether the resulting HTML table is now present in DOM, and then disable it:
    var InfoPathCheck; var maxNrChecks = 10; $(document).ready(function(){ InfoPathCheck = setInterval(function() { //Wait for InfoPath to finish loading HasInfoPathLoaded(); }, 200); }); function HasInfoPathLoaded() { var selectAppl = $("#ctl00_m_g_d1ea83c5_3306_41e4_8419_01cfcf73921e_FormControl0_V1_I1_R5"); if (selectAppl.length > 0 || --maxNrChecks === 0) { clearInterval(InfoPathCheck); if (selectAppl.length > 0) { $(selectAppl).prop('disabled', 'disabled'); } } }

SharePoint mobile app + SharePoint Framework support?

The latest development approach for SharePoint customizations is via the new SharePoint Framework. MVP Waldek Mastykarz has multiple informative posts on what it is and how to utilize. In Everything you need to know about the SharePoint Framework he makes a remarkable statement: "When building solutions on the SharePoint Framework, if you follow the guidelines provided by Microsoft, not only will your solution look great on mobile devices but it will also be rendered in the native SharePoint mobile app which will be released shortly".
Remarkable, as I'm confused by how a native App could be running clientscript. So I challenged Waldek and Bill Baer via twitter to expand on that:
Well, turns out the statement is for now a bit too early. Microsoft is working on bringing this capability in the new SharePoint mobile app, and Vesa Juvonen acknowledges that Microsoft should tackle my question in their message.
UPDATE (June 13) Jeff Teper, interviewed at SharePoint Saturday Paris, answered on my question, being "We host html in JavaScript" [within the native code of the mobile app]. Although this is merely the high-over answer, and I want to understand more of the lower level details; this is sufficient for now. Details will be shared later by Microsoft and informed MVPs.

Wednesday, June 1, 2016

Performance repercussions of the Add-In model

The Add-In model is great to deliver self-contained functionalities, isolated from (harming) the SharePoint farm health - runtime and wrt physical installation. But be aware that there is a price: performance. In general, the overall performance will be negatively impacted due the Add-In model. Let me clarify:
  1. Conceptual every Add-In is an own and independent (mini) webapplication. Requesting a SharePoint page that hosts <X> Add-Ins, effectively means that the browser is visiting 1+<X> webapplications to load and render the page: first the SharePoint hostweb for the SharePoint hostpage, and next the individual AppWebs for the contained Add-Ins (SharePoint-hosted or Provider-Hosted).
  2. Each Provider-Hosted Add-In (re)enters in an own Add-In launch cycle, starting with App-authentication via the _layouts/AppRedirect.aspx page.
    Our renewed SharePoint based intranet heavily applies the Add-In model, with personalized homepage containing between 2-10 Add-Ins (mixture of SharePoint-hosted and Provider-hosted). In preparation of the Go-Live date, I validated performance and scalability through Visual Studio loadtesting. Due the multiplication of Add-In instances on the homepage there are multiple AppRedirect.aspx calls per homepage visit. On increasing the load the SharePoint server execution of that request became the scalability bottleneck, resulting in CPU to reach 100%. Investigation by Microsoft Premier Support confirmed that the high number (higher as what Microsoft architects had foreseen) of AppRedirect.aspx requests caused the peak in CPU:
    • The high CPU is caused by triggering of Garbage Collector
    • Garbage Collector is triggered due large managed heap, with int64 array datastructures
    • The int64 array datastructures are allocated by App Authentication, to encrypt and decrypt SharePoint internal certificates

    Note: As this (CPU reaching 100%) was for load well beyond both the expected / typical as the peak load, it was not needed to qualify as a No-Go for our Go-Live.
  3. Each Add-In is included in the page-html via iframe element. Due iframe boundaries, there is no runtime sharing possible of client-side objects and resources: every iframe must load its own required objects in own runtime browser context (can be from browser cache for static resources, if that is enabled at client and server-side).
  4. Be careful with building (or buying) too much self-contained Add-Ins, that all store the resources used by the Add-In in own AppWeb. Typical examples is sp.js. If that is administrated per AppWeb, then this heavy library will be retrieved multiple times: typical from the hostweb for standard SharePoint handling, and next from AppWeb(s) due Add-In usage. Due different urls, the browser does not recognize them as same resource, that is already retrieved. On each 'first' visit scenario this will result that the browser will retrieve the same resource multiple times, despite that it is already retrieved before and cached in browser.
  5. Resources that are provisioned as Add-In rootcontent are not included in the blobcache. The effect of this is twosome: [1] SharePoint does not cache the content in blobcache, and on every request must get it again from SharePoint content database; and [2] retrieved resources do not include the ‘max-age’ cache-control header setting (see Max-age cache-control setting for SharePoint content), and therefore the browser must every time ask the webserver whether client-cached resource is still valid or changed on the webserver side. Which in case of static resources (e.g., jQuery library) typically will not have changed, always responding with HTTP 304, and therefore wasting requests and network bandwidth + time.
    Via Yammer Office 365 Network I’ve inquired how-to reduce the number of 304 responses for an Add-In, but apparent this answer is not known as no answer is given - also not by any of the connected Microsoft Office 365 architects and engineers.
  6. Due own appdomain's, no sharing of http-connections across hostweb and the Add-Ins, and as result also no sharing of http-authentication context (see SharePoint App-Model + NTLM results in more 401’s).
  7. Client-side altering of the page-DOM results in a reload of all Add-Ins. This is standard iFrame behavior, implemented as such in all current browsers (E.g. iFrame reloading when moving it in DOM).
All of the above enumerated performance-degradation aspects are inevitable when utilization of Add-In model. Our end-users on average are confronted with one or more 'Working on it...' spinners when they open up their homepage filled with Add-Ins. We managed to mitigate for a large part by combination of some asynchronous behaviour, lazy loading, and enforce the Add-Ins to retrieve resources from shared location (hostweb, but can also be a CDN - internal or SharePoint static).
Once we're enabled to utilize the new SharePoint Framework, we will evaluate which of the Add-Ins can be migrated to that new model and thus break out of the iFrame boundaries + constraints. At minimal the SharePoint-hosted Add-Ins are good candidates for migration to SharePoint Framework based solutions.

Sunday, May 8, 2016

Critical thoughts on [yet another] new SharePoint development approach

This Wednesday May 4th, Microsoft announced GA of SharePoint Server 2016. This event is accompanied by blogposts from multiple MVP's. Waldek Mastykarz posted an interesting one "When to use the new SharePoint Framework". However, this posts holds a rather distracting message for us: Solutions built using other development models introduced in the past, such as farm solutions or SharePoint add-ins, cannot be used on modern team sites. Rather then repeating my concerns and thoughts here, I refer to the post of Waldek for full context, and the comments-thread between Waldek and myself.

Tuesday, May 3, 2016

Tip: Steps to resolve from parallel safe-conflict on provisioning a PageLayout

We’ve constructed our SharePoint 2013 based intranet cloud-ready, utilizing the AddIn-model. This for runtime functionalities, examples are Company News, My Favorites, My Tasks, Yammer AddIn's. And conform the Office 365 best-practice also for provisioning of SharePoint branding entities: masterpage(s), PageLayouts, content types. Yesterday I needed to reactivate a (new) version of a provisioning AddIn to update a PageLayout. However, exactly the update of that PageLayout failed at the App activation, with the message: Save Conflict – Your changes conflict with those made concurrently by another user.
The origin of this can be diverse. In my case it was for sure not due an actual parallel safe conflict. I suspect that as side-effect of a previous action, the safe-administration in SharePoint is no longer in sync with the state as original provisioned by the AddIn. But as SharePoint content database is a black box, almost impossible to analyze what caused the safe conflict. However, that is even not that interesting here. What counts is how to resolve such that the updated version of the PageLayout is successful provisioned by the AddIn. Well, this comprises of multiple steps that I outline below.

Approach to resolve from the safe-conflict

In concept this is simple: avoid the occurrence of safe-conflict by removal of the existing PageLayout, and next the new version can be provisioned without errors. However, this is easier said then done as the PageLayout typically is already in-use. And SharePoint is aware of that, and blocks the deletion as long as in use. So a prerequisite is to also and first remove the ‘used-by’ occurrences.

Steps to resolve from the safe-conflicts

  1. Open ‘Content and Structure’ from ‘Site Settings’, and navigate to the ‘Master Page Gallery’
  2. Select the PageLayout that gives the SafeConflict, and click in the top menubar on ‘Show Related Resources’
  3. This will expose whether and by which publishing pages the PageLayout is ‘Used-By’
  4. For each of these pages, edit them and temporarily associate with another PageLayout
  5. After all relevant pages are deassociated from this PageLayout [you can verify this by refreshing ‘Show Related Resources’], you can delete the PageLayout from the masterpage catalog
  6. And now activation of the provisioning AddIn will no longer give a safe-conflict
  7. Afterwards, reassociate all the relevant pages with the (new version of the) PageLayout.

Thursday, April 21, 2016

Freeze pane on SharePoint list

In case of a larger SharePoint list, end-users may appreciate that the header row remains in the visible area while they scroll through the list. Out-of-the-box the SharePoint XsltListView webpart does not provide this functionality. However, it is possible to augment the standard behavior by modifying the rendered HTML through javascript. There are multiple examples published, but for some reason these did not immediate work for me. Did get me inspired though, and I coded a jQuery-snippet that when inserted in a page containing a XsltListView webpart delivers the 'freeze pane':
<script src="https://code.jquery.com/jquery-1.11.2.min.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function () { var $table = $(".ms-listviewtable").first().data("summary", "<listview name>"); // WRAP TABLE IN SCROLL PANE $("#s4-workspace").css( { overflow: 'hidden' } ); $table.wrap("<DIV class='FreezedLV' style='OVERFLOW: auto; HEIGHT: 550px;'></DIV>"); // FROZEN HEADER ROW $(".FreezedLV").wrap("<DIV class='FreezedLVContainer'></DIV>"); $("<table id='FreezedTR' class='ms-listviewtable' cellPadding='1' cellSpacing='0'></table>"). insertBefore(".FreezedLV"); $("#FreezedTR").width($table.width() + "px"); var $origHeader = $("TR.ms-viewheadertr:first", $table); var $firstRowTable = $("TR.ms-itmhover:first", $table); var $freezeHeader = $origHeader.clone(); // Propagate the computed width of columns of origheader to freezeheader $origHeader.children("th").each(function() { var width = $(this).width(); var ownerIndex = $(this).index(); $($freezeHeader.children("th")[ownerIndex]).width(width + "px"); }); // Propagate the implicit width of row columns as explicit widths in first row to // avoid layout-changes when hiding the original header $firstRowTable.children("td").each(function() { var width = $(this).width(); var ownerIndex = $(this).index(); $($firstRowTable.children("td")[ownerIndex]).width(width + "px"); }); $("#FreezedTR").append($freezeHeader); $origHeader.hide(); }); </script>

Tuesday, April 19, 2016

How-to have IE11 pick up local PAC file

In our company we utilize a cloud-hosted Proxy Automatic Configuration script (PAC file). On occasion it is convenient to switch to a local version, e.g. to investigate or troubleshoot. When we still were at IE9 this was easy, but since we’ve upgraded the company browser to IE11 it becomes more difficult. To promote interoperability across network stacks, Microsoft deprecated the option for a local PAC file. You can still configure it in Internet Settings, but IE 11 will simple ignore a local proxy auto-config file.
However, Microsoft also understands it’s backwards compatible responsibility. And therefore build in support to overrule the deprecation of local PAC file. The approach consists of 2 steps:
  • Step 1: [source: Understanding Web Proxy Configuration] Via registry value 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\EnableLegacyAutoProxyFeatures' specify that it is ok to utilize the legacy Internet Settings. Note that in our company the regular users are not enabled to set this, as they are not administrator on their workstation. But the occasional usage of local proxy file is then also not for these regular end-users.
  • Step 2: [source: local proxy which is set in the proxy auto config of IE11 is not processed correctly by IE11] Create the local PAC file as "c:/windows/system32/drivers/etc/proxy", and specify in IE as setting for automatic configuratio script = 'file://c:/windows/system32/drivers/etc/proxy'

Friday, April 1, 2016

Corrupted blobcache hindered responsive rendering

In the branding of our SharePoint 2013 based intranet we utilize Bootstrap for responsive design. However, we noticed that the rendering for mobile user agents was not correct, e.g. the company logo was displayed twice. Remarkable was that in our acceptance environment, it did render correct. On analysis I first discovered that the requests of some resource files (javascript, css) resulted in AccessDenied, but only for mobile UserAgent in the request header:
  • User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) ➜ Downloaded
  • User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 ➜ Not downloaded
Also I noticed that the response for IE as User Agent was successful serviced, but that 'max-age' setting was set to zero in the response (Cache-Control). As on SharePoint webapplication level the Cache-Control header setting is controlled by SharePoint blobcache, this put me in the direction of a blobcache corruptness issue in our production environment. To resolve from the corruptness, our SharePoint operations created a new blobcache for the intranet webapplication. And indeed this resolved both issues: max-age value is set with a positive value (so that lesser times the resource is requested by browser), and also for mobile UserAgents the resource request are now responded with the actual files.

Tuesday, March 29, 2016

Media webpart dependent on older IE versions

When last year we brought our new SharePoint 2013 based intranet life, the company standard browser was still IE9 (the reason in some non-Microsoft webapplicaties, that exposed an hard reliance on older IE version). End 2015 we could move/upgrade to IE11. Today I received a signal from a content owner that the standard Media webpart can not be configured via the ribbon - the Media tab does not popup. I googled and tried some, to find out that this is due the IE upgrade. It appears that the standard Media webpart also has a dependency on older IE version. Pragmatic workaround for content owners is to emulate IE8 via F12 Developer Tools. This is an acceptable workaround for now, as our structural mitigation is to step away from the standard Media webpart (with a reliance on Silverlight) in favor of an html5 based setup, using Video.js library.

Friday, March 18, 2016

Extending List.js with sort on date values

I applied List.js for user-interactive filtering and sorting in a table. However, it turns out that default the List.js sorting is limited to string based only. In this business application there are also columns with date values, so I needed to expand on the basic List.js capability. My approach consists of the following elements:
  • In the ViewModel of the Modern App, also include a data property that contains the timestamp representation of date value
  • In the View, also include an html element to render the timestamp value in the DOM. As you don't want to distract the user, this element must be invisible. An hidden 'input' would serve that purpose, where it not that List.js only retrieves values from block-elements. Alternative therefore is an hidden 'Div' element.
  • In the View, change the List.js data-sort specification to this timestamp value. Also make sure to include in the valueNames specification for instantiating the List
  • Almost there... Earlier I already extended List.js to parse the raw values from html elements. Reason for doing that is that default List.js retrieves the innerHTML. And in case your html element contains html-specification (e.g. for an hyperlink), this results in malfunctioning of List.js filtering/searching and sorting capabilities. I determine the raw values via 'innerText'. However, Chrome returns for hidden elements an innerText value of empty string... Therefore I made an additional minor addition to in such case determine the raw value via 'textContent'.
So far the written explanation. For developers it is much more understandable to explain via code:
The modifications to ViewModel (Knockout)
The modifications to View (Knockout)
The modifications to List.js library

Thursday, February 18, 2016

Directly open attachment from XsltListView UI

A business department composed a knowledge system as SharePoint-enabled Business App. The knowledge items are structured information about defects, with aspects as system, impact, fix available, owner. At a conceptual level, the correct SharePoint information architecture to administrate this is as list items. Some, not all, of the knowledge items can have associated documents. Important to realize is that the document(s) accompanying a knowledge item is/are secondary level information, it is not the main entity. So it is a valid decision to not set up as SharePoint document library.
However, on document level a library has convenient user experience. You click on the title of a document item, and it opens up: in browser or in native client, dependent on library settings and type of the document item. Our business requested the same convenient user experience for those knowledge items that have an associated document as attachment. Direct open from list, avoid the cumbersome usage path to first open the item and from the item dialog open the attachment in case present for the item.
A javascript approach can be applied to modify/extend the native XsltListViewWebPart behavior to deliver on the above. It relies on a combination of dynamic javascript overloading, SharePoint REST service, and SharePoint OWA viewers. Below the code snippet.
var EnrichKnownIssuesView = window.EnrichKnownIssuesView || {}; EnrichKnownIssuesView.UI = function () { function OverloadEditLink() { EditLink2 = function(target,nr) { var href = $(target).attr('href'); var idStart = href.substr(href.indexOf("&ID=") + 4); var id = idStart.substr(0, idStart.indexOf("&")); var siteUrl = window.location.href.substr(0, window.location.href.indexOf("/default.aspx")); if (siteUrl.length > 0) { var getAttachmentsUrl = siteUrl + "/_vti_bin/ListData.svc/KnownIssuesList(" + id + ") ?$expand=Attachments&$select=Id,Attachments"; $.getJSON(getAttachmentsUrl, function (data) { if ( data.d.Attachments != null && data.d.Attachments.results != null && data.d.Attachments.results.length > 0 ) { for (var i = 0; i < data.d.Attachments.results.length; i++) { var attachmentsUrl = siteUrl + "/Lists/Known Issues List/Attachments/" + data.d.Id + "/" + data.d.Attachments.results[i].Name; /* For some files, open in Office Web Applications results in error "Service not available". To prevent that the user then will not be able to view the document, configure the web-address of native document as source --> it will then open in native client. */ var fileName = data.d.Attachments.results[i].Name.toLowerCase(); if (fileName.endsWith(".docx") || fileName.endsWith(".doc")) { attachmentsUrl = siteUrl + "/_layouts/WordViewer.aspx?id=" + encodeURI(attachmentsUrl) + "&Source=" + encodeURI(attachmentsUrl) + "&DefaultItemOpen=1"; } else if (fileName.endsWith(".xlsx") || fileName.endsWith(".xls")) { attachmentsUrl = siteUrl + "/_layouts/xlviewer.aspx?id=" + encodeURI(attachmentsUrl) + "&Source=" + encodeURI(attachmentsUrl) + "&DefaultItemOpen=1"; } else if (fileName.endsWith(".pptx") || fileName.endsWith(".ppt")) { attachmentsUrl = siteUrl + "/_layouts/PowerPoint.aspx?PowerPointView=ReadingView&PresentationId=" + encodeURI(attachmentsUrl) + "&Source=" + encodeURI(attachmentsUrl) + "&DefaultItemOpen=1"; } window.open(attachmentsUrl, '_blank'); } } }); } }; } var ModuleInit = (function() { ExecuteOrDelayUntilScriptLoaded(OverloadEditLink, "inplview.js"); })(); }();
Special attention in the above for the usage of the 'Source' querystring parameter. During testing of the overloaded EditLink2 function, I occassional encountered an error in which OWA reports an error:
Opening the document in native Office client does succeed. I could have resorted to just always let the document open in native Office, but honestly I dislike that. With the OWA usage model, there is no need to download the document to the client system (consider Information Rights Management and Data Loss Protection), nor a need to have MS Office installed on the specific client device. To give that completely up, just for the rare occurence of a document on which OWA opening fails is a pitty. Instead I apply a 2-step approach: I let the Business App first try to open the Office document in OWA, which in most of the cases succeeds. In the rare case this fails, after the user clicks 'away' the above error message, the browser next 'returns' to the url specified in the 'Source' querystring parameter thus opening the document in the native Office client. Via this 2-steps approach, it is assured that the user can view the document: preferable in the browser, or in the rare problem situation in the native Office client. Simple and pragmatic.