- Slow SharePoint processing (IIS, SharePoint code)
- Slow SharePoint content retrieval (SQL, storage)
- Slow or blocking network
- Slow authentication handling (NT Explorer as web client to SharePoint server, e.g. see Prompt for Credentials When Accessing FQDN Sites From a Windows system)
- Slow Web Proxy Auto Detection (see Explorer View very poor performance, Slow response working with WebDAV resources on Windows systems)
- SMB protocol blocked (Source: Microsoft Whitepaper "Understanding and Troubleshooting SharePoint Explorer View")
- Interference with IIS WebDAV Publication Service at SharePoint server (SharePoint – Open with Windows Explorer – problems, 5. WebDAV Publishing, and Explorer view does not work in some scenarios when the SharePoint farm is on Windows Server 2008 R2)
- Conquer with network requests from other local running programs
- Network requests blocked or delayed by anti-virus processing
- Slowness in local WebDAV client processing (Windows Explorer as WebDAV client)
- Outdated local Windows binaries (Explorer binaries (Shell32.dll), WebDAV binaries (Webclnt.dll, Davclnt.dll, Mrxdav.sys) & SMB binaries (Mrxsmb.sys, Mrxsmb20.sys, Rdbss.sys))
- Interference with Internet Explorer AddOn's
Tuesday, December 29, 2015
The long quest to reinstate SharePoint Explorer View performance
Monday, December 21, 2015
Our approach to deliver aligned business solutions
Thursday, December 17, 2015
Beware: List Validation Settings also effective in Workflow execution
- =OR([Created]<[Modified],[State]="Draft")
- =OR((INT(Modified-Created) > 0),(State="Draft"))
- =OR((DATEDIFF(Modified,Created,"s")>0),(State="Draft"))
Wednesday, December 9, 2015
Lessons learned with Add-In update execution
- Add the updated Add-In(s) to the SharePoint Add-In catalog;
- Then first manual update ('Get It') the installed Add-In on the rootweb, for immediate result visible to end-users;
- Next, via powershell script, update all the Add-In instances in the site hierarchy: traverse through the hierarchy, and on each hostweb that has the Add-In installed, execute 'Update-SPAppInstance'. Under the SharePoint hood, this delegates the Add-In update(s) to execution via a timerjob.
- The completion of the Add-In update throughout the site hierarchy is very time-consuming. In our intranet we've experienced elapse time of over 10 hours.
- Until the full completion of the Add-In update, it is not possible to manual update the same Add-In. Situation in which you want / need to do that - and we encountered one - is when in the release it is observed that after the Add-In update, the updated Add-In has an issue. As Add-In rollback is then not longer possible (once a Add-In update is executed), the remedy is to deploy a fix (on the update/fix).
- Most surprising: until the full completion of update of Add-In 'X', also the manual update of any other Add-In 'Y' is blocked for completion.
Wednesday, December 2, 2015
Peculiar but Explained: Access Denied on page in search result preview
Friday, November 20, 2015
Chrome the better performant browser for App/AddIn-model based applications
Thursday, November 12, 2015
Tip: how-to pragmatic and quickly mitigate from malfunctioning Add-In in production
<iframe src="https://.../_layouts/15/appredirect.aspx?redirect_uri=....&client_id=..." id="..." height="250px" frameborder="0" width="720px"></iframe>
<script type="text/javascript"> var iframes = document.getElementsByTagName('iframe'); var i; for (i = 0; i < iframes .length; i++) { var src= iframes[i].src; if (src.indexOf('<AppRederict.aspx url with reference to mailfunctioning Add-In>') != -1) { iframes[i].src = 'https://.../Style%20Library/ Mitigation/CodeSnippetAsAlternativeForMalfunctioningAddIn.aspx' } } </script>
Tuesday, October 27, 2015
Don't: Publishing + 'Anyone who can read items'
Thursday, October 8, 2015
Inconvenient ‘Shared Column’ in Document Set
- a Document Set to collapse all documentation involving a review process – the document to review, and accompanying documentation, review sheets, explanatory documentation, and so;
- the Document Set preset with a document template for document to be reviewed;
- a SharePoint Designer workflow associated with the content type of the document template to steer the review process;
- and a workflow on the docset library to "archive" the docset once the review on contained document is completed.
Thursday, September 24, 2015
Architecture decision: SharePoint-hosted AddIn or 'plain-old Html/JavaScript/CSS'
When does SharePoint-hosted AddIn has value above 'plain-old Html/JavaScript/CSS'?
- Building functionality as AddIn results in extra build effort; code and testing for the AddIn installation, AddIn lifecycle management;
- Deployment is more complex: add to SharePoint AppCatalog, and next add to site collection;
- AddIn based deployment requires the presence of a SharePoint AppCatalog in the webapplication;
- Reusing the AddIn functionality on multiple locations in the site structure, results in multiple AddIn instances that each operate in isolation. While this can be desired, it also means that an AddIn update must be repeated on multiple instances in the site structure; and may result in inconsistent behavior when not all AddIn instances are upgraded;
- Cleanup of AddIn is more complex, and largely happens under the SharePoint covers and outside the control and visibility of site owner. I’ve seen examples of so-called Orphaned Apps (the initial name) that can only with difficulty be removed from the SharePoint content database.
- When the functionality must be customizable; either by content editor ico shared page, or end-user ico personalized page. For such you need AppPart properties, to customize the (individual) App Instance
- When the functionality will be deployed multiple times in different locations (sites, web applications), and with the flexibility to let if behave differently in different locations
- When you want to enable site owners, or even end-users via personalization, self to add and remove the functionality to a page (via the AppCatalog)
- For suppliers to package as direct installable ‘deployment unit’; either via Microsoft Store, or via local AppCatalog
Friday, September 11, 2015
SharePoint: platform, services API, or both?
Monday, August 17, 2015
Project-level Best Practices for delivering a performant Add-in based application
- Performance and Capacity Management must be applied as an integral subject during the application development project
- In the requirements, agree with the application owner on the performance aspects: page download times considered acceptable, applicaton parallel usage
- Include performance Best Practices in the development guidelines. And make sure that all involved developers know of the guidelines, and that they apply them in their individual developed Add-Ins
- Do not only develop + test SharePoint Add-ins ('Apps') in isolement; also conduct integration test with multiple Add-ins on a page as the user is likely to use them, and monitor the page payload
- Thorough intake of any external Add-ins before before purchase; intake on architecture, functionality, capacity management and maintenance aspects
- Structural monitor and proof the application performance during the project, to detect in early stage when something is introduced with a severe performance decrease impact. Load testing is a good means to implement this in the project, for performance quality assurance
Some architectural and technical tips
- Cache where appropriate (but be aware that caching costs resources itself)
- Reduce the number of network roundtrips from client to application server - batch requests
- Retrieve resources that are used in multiple Add-ins from a shared location - e.g. root site of the HostWeb, or a CDN (external or internal)
- Reduce the impact of (NTLM) Authentication by retrieving non-authorized resources from anonymous accessible location
- Utilize the sprites-concept for images
- And the same for custom javascripts; for maintenance it is good to separate responsibilities in different libraries, but for performance it is better to collapse in a single resource file
- Apply minimalization in resource files: javascript and CSS resource files
- Apply lazy loading where appropriate (e.g. avoid processing and retrieval impact for Add-in functionality that is initial non-visible, and/or only rare used; in favor of delayed execution if and only if the user intends to use the Add-in)
Thursday, August 6, 2015
Load testing SharePoint Add-in (former App) Model
Validate healthy application performance behaviour
Essential for any Enterprise Application is that it can performant and scalable handle the varying usage load by the users. Nothing as embarrasing as a new application that soon after Go-Live, breaks by the enthiousastic usage of the users. To prevent such, you must build trust in the scalability of the application, and establish before Go-Live that the application – application software + system infra - can handle the expected load. Introduce load testing.Application Performance health factors
2 health factors monitored:- Responsiveness of the application for the user, measured as Page Download Time
- Scalability of the SharePoint infra, measured as CPU, Memory and I/O utilization on the servers
Application Performance validation approach
- Identify target goals for application utilization
- Proof the health factors at the target-utilization goals via load testing, to simulate the real usage
- Identify the ‘breaking’ point via increased load/stress testing
- Determine the rootcause of the issue; this can be non-optimal code, insufficient infra parts (CPU, memory, network throughput, database IOPS)
- Fix the issue(s)
- Repeat the validation, at step 2
Loadtest execution
loadtest preparation
- Identify the usage/application scenarios you will use to build trust. You should select scenarios for which you expect these will be used during typical usage. An heavy transaction that in the normal operation will only be rare executed, will have a neglectable effect on the application load.
- Establish the target load. This is the application load for average usage. For web applications, this is typically stated in ‘Page Visits per Second’. Note that this is different from Requests per Second / RPS. In nowadays modern apps, a single page visit encompasses multiple http requests: for the page itself, dependent resources as javascript and css, and javascript calls to execute service calls for data retrieval and application functions.The determination/specification of the concrete target value is a challenge on itself. One easily is tempted to overstress the target value - we have 'X' users, so the parallel application usage will be 'X * Y'... However, in reality those 'X' users do not continuously all hit the application: they log on at different times, stay on pages, use other applications, go to the coffee machine, ... In our setup we identified the target value twofold:
- Fact: as we were introducing a renewed intranet, we could reuse the application usage statistics of the current intranet;
- Prediction: determine the target value via Microsoft (Bill Baer) Capacity Management Formula, an unofficial best practice recommendation
- Establish the heavy load: this is abnormal but still foreseenable application usage, in special circumstances.
- Determine how-to build trust: manual load testing, custom test software, or utilize a load test tool – e.g. HP LoadRunner, Visual Studio LoadTest.
- Get sufficient test accounts to simulate different users. This is also required to prevent cache effect during load test execution. E.g. continuous retrieving user profile values of the same user.
- Prepare the test context for the test accounts. E.g. if the application makes use of SharePoint user profile, then the user profile must be provisioned for the test accounts to ensure reasonable load behavior.
Specialities with setting up testscripts for Add-ins / Apps
- The load test scenario must join in App authentication flow. In essence, this means that SPAppToken value must be set as FORM POST parameter in submit request to appredirect.aspx. The value is runtime determined in the App launcher, and returned in the initial AppRedirect.aspx response.In the Visual Studio webtest recording, the reference is made to this hidden field in the response.We encountered that the SPAppToken value is not successful runtime retrieved. This can in some circumstances be corrected by monitoring the traffic via Fiddler, and set SPAppToken to a fixed value that you get from the Fiddler trace.
- FormDigest value returned in JSON response from contextinfo call instead of hidden FORM parameter in response body.Resolution is augment the Visual Studio loadtest: Add a Text Extraction Rule to extract the value from the /_api/contextinfo JSON response.
- Default, Visual Studio LoadTest execution does not mimic browser-cache, resulting that each dependent resource is requested over and over. You can change/fix this by configuring the loadtest script to ‘parseDependentRequests = false’.
- Visual Studio LoadTest does not include the execution of javascript in the browser. If required, the activity of the javascript code must be simulated in the test scripts.
- With multiple provider-hosted Apps in the load test scenario, the Visual Studio loadtest scenario can make error in the runtime construction of the load test recording and assign a wrong {app_?} value. In such case, you must manually add a '<ContextParameter Name="AppId_1" Value="<APP domain value>" />', and correct the relevant Requests in the script to send request to the correct app-domain:
- Visual Studio LoadTest recording misses to set header variable ‘Origin’, which hinders CORS protocol handling.
- You can easily overwhelm the usage load by setting the ‘concurrent user’ configuration value. The use of this configuration parameter is misleading: it does not really simulate actual users. It merely sets the threads in the loadtest execution from which to continuously execute the webtest(s) in the loadtest scenario. Per thread, after finishing the webtest, the execution halts for the thinktime value; and then repeats. If you set the thinktime to zero – which is what Microsoft advices on Technet, "Don't use think times…" -, the effect is that continuously requests are fired against your application. The load on the application is then much higher as the value configured in ‘concurrent users’.
- Visual Studio loadagent itself can become the limiting factor. If you want to simulate a larger concurrent usage, this results in equal large set of threads in the Visual Studio execution, all of which busy to execute and monitor a webtest instance. The cpu on the load agent grows to 100%, and the load does not linear increase with the number of ‘concurrent users’ aka threads.
Load test monitoring
- CPU, memory and disk IO per server: WFE, SharePoint backend, AppHost
- State of the IIS queue on WFE and AppHost
- Page download times
- Slowest pages
Interpretation of load test output
- The (average) Page Response Time is the summation of the download time for that request, AND augmented with including the download times of all dependent requests beneath that main request.
- The RPS / Requests per Seconds output is not fit to determine whether the application + infrastructure can handle the foreseen application usage. The application usage translates in Page Visits per Second, in which each page visit typically encompasses multiple (http) requests: the .aspx request, requests for javascript and css resources. In the App execution model, each App launch on the page is in effect an own page visit. As result, the RPS factor is of little use. You must measure the ‘Page Visits per Second’ factor. Pragmatic way to monitor this is to set the thinktime for webtest on 1 minute; so that each minute the webtest is executed. The ‘Page Visits per Second’ factor then equals the Visual Studio reported 'Test per Second'.
References
- Determine target load value
- Plan for performance and capacity management in SharePoint Server 2013
- Estimate performance and capacity requirements for enterprise intranet collaboration environments (SharePoint 2013)
- How to determine Farm Throughput Requirements
- Performance testing for SharePoint Server 2013
- Howto calculate nr of WFEs needed
- How To Calculate Number of web front end servers in a SharePoint farm 2013 and 2010
- How to Make SharePoint 2013 Infrastructure Highly Available
- Troubleshooting SharePoint slugginess
- Simulation of browser caching in VSTS load tests and Web tests
- Web and Load Test FAQs
- Performance Analysis SharePoint 2013
Understand the difference between users and requests
Infra Capacity Management
Visual Studio load testing
Wednesday, July 29, 2015
Handy resource for Excel Services external data troubleshooting
- Functional data managers / compliance officers: maintain the data offline in Excel worksheets, and when the data maintenance effort is finished publish the worksheet as datasource to SharePoint document libary
- A separate 'View' dashboard connects via Excel Services to the 'datasource' Excel worksheets, and renders the dashboard - charts, KPIs and so on; This 'view' dashboard worksheet is via Excel Web Access rendered on the SharePoint dashboard page
Excel Services - 400 Bad Request due large cookie
Friday, July 24, 2015
Excel SaveAs to SharePoint failing due required document library properties
- make the field / document library property non-required,
- or modify the field / document library property to have a default value
Wednesday, July 22, 2015
Convert Excel file into Excel Services compliant variant
- VBA code,
- Macros,
- Comments,
- Shapes,
- External Links, to external workbooks
- (Formula) Names that refer to external workbooks,
- Data validations,
- space(s) in name of worksheet connected as Range Name
VBA Code
Sub SaveWorkbookAsNewFile() Dim ActSheet As Worksheet Dim ActBook As Workbook Dim CurrentFile As String Dim NewFileName As String Dim NewFileType As String Dim NewFile As String Dim ws As Worksheet Application.ScreenUpdating = False CurrentFile = ThisWorkbook.FullName ThisWorkbook.Save RemoveDataValidations ActBook:=ActiveWorkbook RemoveComments ActBook:=ActiveWorkbook RemoveShapes ActBook:=ActiveWorkbook BreakLinks ActBook:=ActiveWorkbook RemoveNamesToExternalReferences ActBook:=ActiveWorkbook RemoveConditionalFormatting ActBook:=ActiveWorkbook RemoveSpacesFromWorksheets ActBook:=ActiveWorkbook RemoveVBA ActBook:=ActiveWorkbook NewFileType = "Excel Workbook (*.xlsx), *.xlsx," & _ "All files (*.*), *.*" NewFile = Application.GetSaveAsFilename( _ InitialFileName:=NewFileName, _ fileFilter:=NewFileType) If NewFile <> "" And NewFile <> "False" Then ActiveWorkbook.SaveAs Filename:=NewFile, _ FileFormat:=xlOpenXMLWorkbook, _ Password:="", _ WriteResPassword:="", _ ReadOnlyRecommended:=False, _ CreateBackup:=False End If ThisWorkbook.Close False Workbooks.Open CurrentFile, False Application.ScreenUpdating = True End Sub Sub RemoveVBA(ActBook As Workbook) On Error Resume Next Dim Element As Object With ActBook.VBProject For Each Element In .VBComponents .VBComponents.Remove Element Next For x = .VBComponents.Count To 1 Step -1 .VBComponents(x).CodeModule.DeleteLines 1, .VBComponents(x).CodeModule.CountOfLines Next x End With End Sub Sub RemoveDataValidations(ActBook As Workbook) Dim ws As Worksheet For Each ws In ActBook.Worksheets ws.Cells.Validation.Delete Next ws End Sub Sub RemoveComments(ActBook As Workbook) Dim ws As Worksheet Dim xComment As Comment For Each ws In ActBook.Worksheets For Each xComment In ws.Comments xComment.Delete Next Next ws End Sub Sub RemoveShapes(ActBook As Workbook) Dim ws As Worksheet Dim sh As Shape For Each ws In ActBook.Worksheets For Each sh In ws.Shapes sh.Delete Next sh Next ws End Sub Sub BreakLinks(ActBook As Workbook) Dim Links As Variant Links = ActBook.LinkSources(Type:=xlLinkTypeExcelLinks) For i = 1 To UBound(Links) ActBook.BreakLink _ Name:=Links(i), _ Type:=xlLinkTypeExcelLinks Next i End Sub Sub RemoveNamesToExternalReferences(ActBook As Workbook) Dim nm As Name For Each nm In ActBook.Names If InStr(nm.RefersTo, "[") <> 0 Then nm.Delete End If Next End Sub Sub RemoveConditionalFormatting(ActBook As Workbook) Dim ws As Worksheet For Each ws In ActBook.Worksheets ws.Cells.FormatConditions.Delete Next ws End Sub Sub RemoveSpacesFromWorksheets(ActBook As Workbook) Dim ws As Worksheet For Each ws In ActBook.Worksheets ws.Name = Replace(ws.Name, " ", "_") Next ws End Sub
Sunday, July 12, 2015
Tabbed view on document library
- Build a custom webpart. However, this is so old-school SharePoint platform utilization; and in our company by default disallowed.
- Build a custom HTML / javascript UI (App), and connect via SharePoint webservices. Although this setup nicely fits in ‘modern SharePoint App-development’, for this specific scenario the drawback is that you then also need to develop yourself all of the Office integration that SharePoint standard delivers for a rendered document library (via ECB menu).
- Reuse XsltListViewWebPart, but specify an own Xslt-styling. This approach suffers from the same approach as the ‘modern App’ alternative: you’re then required to include in the custom Xstl all the code to render Office-integration friendly.
- Reuse XsltListViewWebPart, and dynamically modify the standard grouped-by layout into a tabbed view. Beyond reuse of the native Office-integration, this approach also reuses the lazy loading per group that is native in the XsltListViewWebPart group-by handling. Especially with a larger document library, this makes it much more performant as to retrieve the entire contents at once.
Client-side transfrom group-by layout into tabbed view
The transformation of the standard group-by layout into a tabbed view can be achieved as full client-side code only. To achieve the effect, I inspected the standard delivered html; and next coded the transformation logic in jQuery.Particulars
- The native 'group-by' functionality renders the header(s) of the groups. In a tabbed-view layout, the selected tab however already visualizes which group selected; and the group-headers are undesirable in the rendering.
- The native 'group-by' functionality opens new group in addition to the one(s) already open. For a tab-view experience, the views must be exclusive, act as toggles. Select one tab, automatically closes the tab selected before.
- The native 'group-by' functionality also includes a 'remember' function: by default a grouped-by layout opens with the group(s) opened as when the visitor was last on the page. For a consistent user-experience, it is then required to pre-select the associated tab-button.
The 'App' code
<style type="text/css"> .et-tab { <ommitted…> } .et-tab-active { <ommitted…> } .et-tab-inactive { <ommitted…> } .et-separator { height: 5px; background-color: rgb(134, 206, 244); } </style> <script> var TabbedListView = window.TabbedListView || {}; TabbedListView.UI = function () { function MonthToInt(month) { <ommitted…> } function getCookieValue(cookieName) { if (document.cookie.indexOf(cookieName) != -1) { var cookies = document.cookie.split("; "); for (var cookieSeq in cookies) { var cookieSpec = cookies[cookieSeq]; if (cookieSpec.indexOf(cookieName) != -1 && cookieSpec.indexOf("=") != -1 ) { return unescape(cookieSpec.split("=")[1]); } } } return undefined; } function TabbedView() { var tabrow = $("<div class='et-tabrow'></div>"); $(".ms-listviewtable") .before($(tabrow)) .before("<div class='et-separator'></div>"); $(".ms-listviewtable").children().each(function(i) { // Grouping-row: level 0 or level 1 if ($(this).attr("groupString") !== undefined) { // Month - lowest group level. if ($(this).children("[id='group1']").length > 0) { var action = $("<a></a>"); // Set the buttonlabel := '<month> <year>' by extracting // the values from the original headings. var monthValue = $(this).find("a").parent().clone() .children().remove().end().text().split(" : ")[1]; var parentId = $(this).attr('id') .substring(0, $(this).attr('id').length - 2); var group0 = $(this).parent().children("[id='" + parentId + "']"); var yearValue = $(group0).find("a").parent().clone() .children().remove().end().text().split(" : ")[1]; $(action).text(monthValue + " " + yearValue); $(action).click(function() { var parentId = $(this).parent().attr('id'); var parentTBodyId = "titl" + parentId.substring(0, parentId.length -2; var actualAA = $(".ms-listviewtable") .find("tbody[id='" + parentTBodyId + "']").find("a"); if ($(actualAA).find('img').attr('src') .endsWith("plus.gif") ) { $(actualAA).trigger('click'); } var actualA = $(".ms-listviewtable") .find("tbody[id='titl" + parentId + "']").find("a"); $(actualA).trigger('click'); if ($(this).parent().hasClass("et-tab-inactive")) { $(".ms-listviewtable").children().each(function(i) { if ($(this).attr("groupString") !== undefined) { $(this).hide(); } }); $(".et-tabrow").children().each(function(i) { if ($(this).hasClass("et-tab-active")) { $(this).find("a").click(); } }); $(this).parent().removeClass("et-tab-inactive"); $(this).parent().addClass("et-tab-active"); } else { $(this).parent().removeClass("et-tab-active"); $(this).parent().addClass("et-tab-inactive"); } }); // Add 'tab-button' to tab-row; in chronological sorted order. var button = $("<span class='et-tab'></span>"); $(button).attr('id', $(this).attr('id').substring(4, $(this).attr('id').length)); $(button).append($(action)); var totalMonths = parseInt(yearValue) * 12 + MonthToInt(monthValue); $(button).data('TotalMonths',totalMonths); var added = false; $(".et-tabrow").children().each(function(i) { if (!added && parseInt($(this).data("TotalMonths")) > totalMonths) { $(this).before($(button)); added = true; } }); if (!added) $(tabrow).append($(button)); $(button).addClass("et-tab-inactive"); } $(this).hide(); } }); ExecuteOrDelayUntilScriptLoaded(function() { var cookieValue = getCookieValue("WSS_ExpGroup_"); var group1Opened = false; if (cookieValue !== undefined) { var expGroupParts = unescape(cookieValue).split(";#"); for (var i = 1; i < expGroupParts.length - 2; i++) { if (expGroupParts[i+1] !== "&") { group1Opened = true; break; } else { i++; } } } if (group1Opened) { // XsltListViewWebPart standard behaviour includes a 'remember' // functionality: open the group(s) that was/were open before // refreshing the page with the grouped-view. Overload that behaviour // to make sure the 'tab-row' state is consistent with that. $.prototype.base_ExpColGroupScripts = ExpColGroupScripts; ExpColGroupScripts = function(c) { var result = $.prototype.base_ExpColGroupScripts(c); $(".ms-listviewtable").find("tbody[isLoaded]").each(function(i) { if ($(this).find("td").text() === 'Loading....') { var bodyId = $(this).attr('id') .substring(4, $(this).attr('id').length-1); var tabButton = $(".et-tabrow") .children("[id='" + bodyId + "']"); if ($(tabButton).hasClass("et-tab-inactive")) { $(tabButton).removeClass("et-tab-inactive"); $(tabButton).addClass("et-tab-active"); } } }); // Reset function ExpColGroupScripts = $.prototype.base_ExpColGroupScripts; return $(result); }; } else { $(".et-tabrow span:first-child").find("a").trigger('click'); } }, "inplview.js"); $(".ms-listviewtable").show(); } var ModuleInit = (function() { $(".ms-listviewtable").hide(); _spBodyOnLoadFunctionNames.push("TabbedListView.UI.TabbedView"); })(); // Public interface return { TabbedView: TabbedView } }(); </script>
Update: support for multiple XsltListViewWebParts on page
The above 'App' code works fine in case of a single XsltListViewWebPart on page. However, in our company we also have document dashboards that give entrance to 'archived' and 'active' documents. The above code requires some update to be usable for 1 or more XsltListViewWebPart instances on a single page.<style type="text/css"> <ommitted…> </style> <script> var TabbedListView = window.TabbedListView || {}; TabbedListView.UI = function () { function MonthToInt(month) { <ommitted…> } function getCookieValue(cookieName) { var cookieNameLC = cookieName.toLowerCase(); if (document.cookie.toLowerCase().indexOf(cookieNameLC) != -1) { var cookies = document.cookie.split("; "); for (var cookieSeq in cookies) { var cookieSpec = cookies[cookieSeq]; if (cookieSpec.toLowerCase().indexOf( cookieNameLC) != -1 && cookieSpec.indexOf("=") != -1) { return unescape(cookieSpec.split("=")[1]); } } } return undefined; } var triggerCtxIsInit = false; function initTabSelection(webpartId) { var lstVw = $('div[WebPartID^="' + webpartId + '"]'); var cookieValue = getCookieValue("WSS_ExpGroup_{" + webpartId + "}"); var group1Opened = false; if (cookieValue !== undefined) { var expGroupParts = unescape(cookieValue).split(";#"); for (var i = 1; i < expGroupParts.length - 2; i++) { if (expGroupParts[i+1] !== "&") { group1Opened = true; break; } else { i++; } } } if (group1Opened) { // XsltListViewWebPart standard behaviour includes a 'remember' // functionality: open the group(s) that was/were open before // refreshing the page with the grouped-view. Overload that // behaviour to make sure the 'tab-row' state is consistent with that. if ($.prototype.base_ExpColGroupScripts === undefined) { $.prototype.base_ExpColGroupScripts = ExpColGroupScripts; ExpColGroupScripts = function(c) { var result = $.prototype.base_ExpColGroupScripts(c); $(".ms-listviewtable").find("tbody[isLoaded]").each(function(i) { if ($(this).find("td").text() === 'Loading....') { var bodyId = $(this).attr('id') .substring(4, $(this).attr('id').length-1); var tabButton = $(".et-tabrow").children("[id='" + bodyId + "']"); if ($(tabButton).hasClass("et-tab-inactive")) { $(tabButton).removeClass("et-tab-inactive"); $(tabButton).addClass("et-tab-active"); } } }); return $(result); }; } } else { triggerCtxIsInit = true; $(lstVw).parent().find(".et-tabrow span:first-child") .find("a").trigger('click'); triggerCtxIsInit = false; } } function TabbedView() { $(".ms-listviewtable").each(function(i) { ExecTabbedView($(this)); }); } function ExecTabbedView(lstVw) { var tabrow = $("<div class='et-tabrow'></div>"); $(lstVw).before($(tabrow)).before("<div class='et-separator'></div>"); $(lstVw).children().each(function(i) { // Grouping-row: level 0 or level 1 if ($(this).attr("groupString") !== undefined) { // Month - lowest group level. if ($(this).children("[id='group1']").length > 0) { var action = $("<a></a>"); // Set the buttonlabel := '<month> <year>' by extracting // the values from the original headings. var monthValue = $(this).find("a").parent().clone().children() .remove().end().text().split(" : ")[1]; var parentId = $(this).attr('id') .substring(0, $(this).attr('id').length - 2); var group0 = $(this).parent().children("[id='" + parentId + "']"); var yearValue = $(group0).find("a").parent().clone().children() .remove().end().text().split(" : ")[1]; $(action).text(monthValue + " " + yearValue); // Add clickhandler to: // - check the 'parent-group-header in the table whether already // opened; if not trigger it to open. This is required to reuse // the standard XsltListViewWebPart behaviour wrt remember // state upon refresh. // - invoke the 'original' one of the group-header A in the // table; to trigger the default behaviour // - if 'selected': // - hide the headings that are visualized by the default // clickhandler // - deselect the 'tab' that is current active // - visualize the 'tab' to display as active // - if 'deselected' // - visualize the 'tab' to display as inactive $(action).click(function() { // On first user-initiated click; reset the overload of // ExpColGroupScripts as only applicable on initialization. if (!triggerCtxIsInit && $.prototype.base_ExpColGroupScripts !== undefined ) { ExpColGroupScripts = $.prototype.base_ExpColGroupScripts; $.prototype.base_ExpColGroupScripts = undefined; } var parentId = $(this).parent().attr('id'); var tabrow = $(this).parents('div[class^="et-tabrow"]'); var lstVw = $(tabrow).parent() .find('table[class^="ms-listviewtable"]'); var actualAA = $(lstVw).find("tbody[id='titl" + parentId.substring(0, parentId.length -2) + "']") .find("a"); if ($(actualAA).find('img').attr('src') .endsWith("plus.gif") ) { $(actualAA).trigger('click'); } var actualA = $(lstVw).find("tbody[id='titl" + parentId + "']").find("a"); $(actualA).trigger('click'); if ($(this).parent().hasClass("et-tab-inactive")) { $(lstVw).children().each(function(i) { if ($(this).attr("groupString") !== undefined) { $(this).hide(); } }); $(tabrow).children().each(function(i) { if ($(this).hasClass("et-tab-active")) { $(this).find("a").click(); } }); $(this).parent().removeClass("et-tab-inactive"); $(this).parent().addClass("et-tab-active"); } else { $(this).parent().removeClass("et-tab-active"); $(this).parent().addClass("et-tab-inactive"); } }); // Add 'tab-button' to tab-row; in chronological sorted order. var button = $("<span class='et-tab'></span>"); $(button).attr('id', $(this).attr('id') .substring(4, $(this).attr('id').length)); $(button).append($(action)); var totalMonths = parseInt(yearValue) * 12 + MonthToInt(monthValue); $(button).data('TotalMonths',totalMonths); var added = false; $(tabrow).children().each(function(i) { if (!added && parseInt($(this).data("TotalMonths")) > totalMonths) { $(this).before($(button)); added = true; } }); if (!added) $(tabrow).append($(button)); $(button).addClass("et-tab-inactive"); } $(this).hide(); } }); var webpartId = $(lstVw).parents('div[WebPartID^!=""]').attr('WebPartID'); ExecuteOrDelayUntilScriptLoaded( function () { initTabSelection(webpartId) }, "inplview.js"); $(lstVw).show(); } var ModuleInit = (function() { $(".ms-listviewtable").each(function(i) { $(this).hide(); }); _spBodyOnLoadFunctionNames.push("TabbedListView.UI.TabbedView"); })(); // Public interface return { TabbedView: TabbedView } }(); </script>
Tuesday, May 19, 2015
Beware: BLOB cache may miss modifications via SPD
Wednesday, May 13, 2015
Takeaways from SharePoint YamJam
We'll share more details soon, at a high level patching remains thru MSPs (w/ significant reduction), but now with 0 downtime, removing dependencies between FE and BE components and now upgraders are all done online.
My experience, though it may change: When I installed Office 2016 it removed my InfoPath 2013 on my computer. May have been a bug or something still being worked on.
I think it'll be hard to give as it'll vary based on each organizations needs. The SharePoint Team Site is still the "big" collaboration solution with many libraries, workflows, metadata, term store, etc... It's like the ECM.
Groups is more about Team Collaboration. It pulls things from different products and provides an easy to consume solution that works well from anywhere and on any device with the updates coming.
I have a Group for:
- Blogs
- A Specific project I am doing with others (O365 Guide)
- Sharegate Marketing (especially for Calendar and OneNote)
and anyone can create a new group and get started.
The Team Site is a little bigger and requires heavier thinking, Content Types, how to place everything so that it makes sense etc.
It'll come down to knowing SharePoint vs knowing O365 Experiences and compare to see which fits best for your customers individual needs.
The reality is the users in the organizations are already using other things all over the internet for free or low cost $ per user per month. Bypassing IT altogether.
Groups provides an alternative they can consume without having to go to IT to request a heavy duty Site that requires SharePoint Training (even though it's SharePoint behind it)
Sunday, May 3, 2015
Beware of script-dependencies with AMD loading
Code inspection
Basically, the above code instructs require.js to first load the library ‘/Scripts/AppHelpers.js’, once that is loaded to load jQuery library, and once that is loaded, load a bunch of other libraries that are a.o. dependent on jQuery. And when all libraries loaded, invoke a custom initialization function (not displayed here, as not relevant for the issue).require(['../Scripts/AppHelpers'], function () { var spHostUrl = decodeURIComponent(getQueryStringParameter('SPHostUrl')); var hostProtocol = spHostUrl.split("//")[0]; var hostRoot = spHostUrl.split("//")[1].split("/")[0]; spHostUrl = hostProtocol + "//" + hostRoot; require([spHostUrl + '/Style%20Library/Scripts/jquery-1.11.1.min.js'], function () { require([spHostUrl + '/_layouts/15/MicrosoftAjax.js', spHostUrl + '/_layouts/15/init.js', spHostUrl + '/_layouts/15/sp.runtime.js', spHostUrl + '/_layouts/15/sp.js', spHostUrl + '/_layouts/15/sp.requestexecutor.js', spHostUrl + '/_layouts/15/sp.core.js', spHostUrl + '/_layouts/15/sp.init.js', spHostUrl + '/_layouts/15/ScriptResx.ashx?culture=en%2Dus&name=SP%2ERes', spHostUrl + '/_layouts/15/sp.ui.dialog.js', "../Scripts/jquery.rotate.js", "../Scripts/moment.min.js", "../Scripts/moment-timezone.min.js", "../Scripts/ListController.js", "../Scripts/UserSettings.js", "../Scripts/sp.communica.js", "../Scripts/App.js"], function () { jQuery(document).ready(function () { initialize(function () { }); }); }); });
Runtime analysis, via Fiddler and F12
In Fiddler, often however not always, the following sequence of requests is visible.Explanation: asynchronous loading + library-dependency
In the above displayed App HTML code, you see that in 3rd require.js load handling, a set of libraries are requested for load on the same level. Crucial here is that:"../Scripts/moment.min.js", "../Scripts/moment-timezone.min.js",
Solution
There are 2 alternative approaches to resolve the behavior. Essence of both is to make sure that moment.min.js is loaded before the dependent library moment-timezone.min.js:- Extend on the above code-pattern of explicit separating the load of libraries: still retrieve moment.min.js in the 3rd level, and move the load of moment-timezone.min.js to a 4th level:
require(['../Scripts/AppHelpers'], function () { var spHostUrl = decodeURIComponent(getQueryStringParameter('SPHostUrl')); var hostProtocol = spHostUrl.split("//")[0]; var hostRoot = spHostUrl.split("//")[1].split("/")[0]; spHostUrl = hostProtocol + "//" + hostRoot; require([spHostUrl + '/Style%20Library/Scripts/jquery-1.11.1.min.js'], function () { require([spHostUrl + '/_layouts/15/MicrosoftAjax.js', spHostUrl + '/_layouts/15/init.js', spHostUrl + '/_layouts/15/sp.runtime.js', spHostUrl + '/_layouts/15/sp.js', spHostUrl + '/_layouts/15/sp.requestexecutor.js', spHostUrl + '/_layouts/15/sp.core.js', spHostUrl + '/_layouts/15/sp.init.js', spHostUrl + '/_layouts/15/ScriptResx.ashx?culture=en%2Dus&name=SP%2ERes', spHostUrl + '/_layouts/15/sp.ui.dialog.js', "../Scripts/jquery.rotate.js", "../Scripts/moment.min.js", function () { require(["../Scripts/moment-timezone.min.js", "../Scripts/ListController.js", "../Scripts/UserSettings.js", "../Scripts/sp.communica.js", "../Scripts/App.js"], function () { jQuery(document).ready(function () { initialize(function () { }); }); }); });
- Configure Require.js to be aware of the Module dependency
requirejs.config({ shim: { 'moment-timezone.min': ['moment.min'] } });
Friday, April 17, 2015
CQWP, PictureLibrary and Blobcache
- On server side: actual images can be cached in blobcache, but preview images and also thumbnails are by SharePoint design not cached in blobcache. The reasoning is that blobcache is for content that is multiple times (often) retrieved, while preview images are typically only of interest for content manager upon functional management of the picture library contents. As result, each request for a preview image means that SharePoint needs to retrieve it from the content database.
- On client, network + server side: browser cache can be applied to avoid the browser over and over requesting same image. But the browser then still needs to query the server whether the cached resource is unmodified at the server (response 304). For images / static resources that do not change (often), even this request can be avoided: minimizing request/response handling between client (browser), server, and the network transfer. Browsers support this via ‘max-age’ setting. SharePoint supports this ‘max-age’ setting for SharePoint content, via… Blobcache. For SharePoint content retrieved outside blobcache, as thus preview images, the max-age value is not set in the http response. As result, the browser will query the remote SharePoint server whether the image is unmodified, and the server responds with '304 NotModified'. And this can end-up in some noticeable latency, dependent on how busy the SharePoint server is:
- In the ItemStyle.xsl, change the rendering specification to display ‘EncodedAbsUrl’ iso ‘ImageUrl’;
- And you need to modify the ‘CommonViewFields’ specification of CQWP instance, to also include ‘EncodedAbsUrl’.