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