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); } } }