Tagged: JavaScript
Updating Legacy SharePoint Customizations to the Add-in Model
If you follow this blog, you have probably read many of my posts through the years dedicated to the Add-in (formerly app) model for SharePoint 2013, 2016, and SharePoint Online. Microsoft introduced the Add-in model in SharePoint 2013 to address the various shortcomings associated with running custom code on the SharePoint server — most notably, of course, that custom code cannot be deployed to SharePoint Online in Office 365. SharePoint-hosted Add-ins are great for simple functionality that can be achieved with 100% client-side solutions (HTML, CSS, and JavaScript), while provider-hosted Add-ins allow you to run custom server-side code in Azure or anywhere other than the SharePoint server.
Using the Add-in model is a no-brainer for most new development efforts, especially if you are developing for SharePoint Online and still need to write server-side code. (At the time of this writing, the new SharePoint Framework is still in Developer Preview but is definitely worth adding to your repertoire for client-side SharePoint development in the years to come!)
But what about your existing solutions? Many organizations using SharePoint maintain large portfolios of custom code solutions that leverage different legacy development approaches advocated by Microsoft through the years, and most of the custom code developed for earlier versions of SharePoint cannot be directly migrated to the cloud. If your organization decided to move to the cloud tomorrow, where would you begin?
Taking inventory of your customizations
I have found that most legacy SharePoint customizations can be placed into the following categories:
- User interface enhancements (master page, branding, themes, custom web parts)
- Declarative items (XML for custom site columns, list instances, and content types)
- Timer jobs (and other administrative extensions to the platform) running in SharePoint Central Administration on-premises
These customizations may take the form of farm or sandboxed solutions that depend on SharePoint’s legacy feature framework. In the case of some user interface customizations, custom HTML markup, CSS, or even JavaScript might be embedded directly within a site’s master page file. For the most part, these are situations we should try to avoid as much as possible when migrating our customizations to the cloud for the following reasons:
- Farm solutions cannot be deployed to SharePoint Online.
- Sandboxed solutions that contain managed code can no longer be deployed to SharePoint Online (note that sandboxed solutions contain a managed code assembly by default, even if the solution does not contain any managed code).
- Custom master pages, while still supported in SharePoint Online, should be avoided unless absolutely necessary. (Microsoft has a history of making significant changes to the default master page in SharePoint Online, such as the addition of the app launcher/waffle/Hollywood Squares in the upper left corner. Sites with heavily customized master pages may not always have access to these changes as they are introduced in the future.)
The role of the Add-in model
When migrating these customizations, keep in mind that you will not necessarily be building and deploying a SharePoint Add-in to replace each customization. In fact, in many cases, you will simply be leveraging techniques from the Add-in model. These include paradigms that were introduced and/or popularized with the introduction of the Add-in model:
- Using client-side script wherever possible
- Remote provisioning of assets (everything from CSS/JavaScript files to site columns, content types, etc.) using the .NET Managed client object model (CSOM) or PowerShell
- Running server-side code outside the SharePoint server that communicates with SharePoint via its APIs
In some cases, all you may need to do is build and deploy a “throwaway” provider-hosted Add-in within a local development environment that performs some one-time remote provisioning tasks, but is not intended to be accessed by end users and can be removed after its work is done.
Add-in model techniques enable us to make our SharePoint customizations with a “lighter touch.” By lighter touch, I am referring to a lighter footprint and/or impact on the SharePoint server. As we decrease the burden on the SharePoint server, we enable SharePoint to run more reliably and efficiently while also making it easier to administer and upgrade (at least on-premises…yes, these techniques all work on-prem as well!) To better explain this concept, I put together the table below contrasting legacy SharePoint development approaches with their modern lighter touch equivalents:
Legacy Approach | Lighter Touch |
---|---|
Farm solutions with custom code running in the SharePoint IIS worker process | Provider-hosted Add-in with custom code running outside of SharePoint |
Farm or sandboxed solutions that deploy declarative artifacts such as site columns, content types, and list instances | Remote provisioning these artifacts from a provider-hosted Add-in using CSOM |
Custom master pages with embedded markup, styles, and script | Remote provisioning of branding assets, embedding of JavaScript UI customizations using ScriptLink custom actions |
Custom timer jobs running in Central Administration using the SharePoint server object model | Remote timer jobs running as Windows scheduled tasks or Azure WebJobs using CSOM |
SharePoint Developer PnP: here to help!
Because the Add-in model represents such a strong departure from the way things were done in the past with full-trust code in SharePoint, Microsoft started the SharePoint Developer Patterns and Practices (or PnP) initiative to assist developers with transforming their existing solutions to be cloud-ready as they migrate to SharePoint Online in Office 365.
The PnP team maintains several Github repositories that include reusable, production-ready components, templates, and solution starters that demonstrate the preferred modern approach to SharePoint development — making customizations with a lighter touch wherever possible.
Eric Overfield just published a great blog post detailing the PnP initiative and how you can get involved.
My Pluralsight course
If you are interested in learning more, I recently published a Pluralsight course with lots of demos: Updating Legacy SharePoint Customizations to the Add-in Model. The course covers the process of migrating a heavily customized on-premises SharePoint site to SharePoint Online from start to finish. The clip below is a demo from the course showing how a legacy timer job customization can be made cloud-ready with the help of the PnP Timer Job Framework and Azure WebJobs.
In this course, you will see me leverage the PnP Core Component to drastically simplify writing .NET Managed CSOM code. I also use several great samples and solution starters from the PnP team, all with the objective of easing the pain associated with migrating legacy SharePoint customizations to the Add-in model.
Custom site header and footer using a SharePoint-hosted add-in
In this post, I will show you how to create a custom site header and footer for SharePoint Online/Office 365 that will render on all pages using a SharePoint-hosted add-in. This could be used to post critical alerts or to specify the level of business impact of the content stored within a particular site (e.g., high, medium, or low).
This add-in will accomplish the following, all without a single line of server-side code:
- Upon installation, deploy 3 JavaScript files to the Style Library of the host web
- Register 3 user custom actions in the host web to ensure these JavaScript files are loaded on every page
- Via an add-in part, provide a mechanism for users to enable/disable and set properties of the custom header and footer (e.g., message text, background/text color)
- Store and retrieve configuration parameters for the site header and footer in the host web’s property bag
Rendering a custom header and footer
We might be tempted to edit the master page directly to add a header and a footer, but this is not considered a best practice (especially in SharePoint Online, where master page changes can be rolled out quite frequently). Instead, our add-in will make use of JavaScript techniques to insert
- In SharePoint Online/Office 365, our header (with ID customHeader) will be inserted above thewith ID suiteBarTop (for SharePoint 2013 on-premises, you’ll need to update the script to reference ID suiteBar instead).
- Our footer
(with ID customFooter) will be inserted below thewith ID s4-bodyContainer.- NOTE: The
IDs in SharePoint Online are never set in stone and could change at any time. If they do, you will need to update HeaderFooter.js to reference the newIDs.jQuery makes it easy to create our custom header and footer
elements and insert them in the appropriate location within the DOM with its .insertBefore() and .insertAfter() functions following this approach:$("
HEADER TEXT").insertBefore("#suiteBarTop"); $(" ").insertAfter("#s4-bodyContainer");Making the footer “sticky”
Huge shout-out to Randy Drisgill for his SharePoint 2013 Sticky Footer implementation. The “sticky” footer anchors the footer
to the bottom of the page (or the bottom of the browser window if the available screen real estate exceeds the amount of content on a given page). I only needed to make one change to Randy’s StickyFooter.js implementation, and that was to account for the height offset imposed by the addition of our customHeader:var difference = windowheight - (bodyheight + ribbonheight + footerheight + $("#customHeader").outerHeight());
Provisioning files to the host web via JavaScript
For anyone following the OfficeDev PnP, the concept of remote provisioning to allow add-ins to deploy files to the host web should be a familiar one. However, the PnP examples make use of the .NET Managed CSOM to do this, which is a perfectly valid technique but would require us to develop a provider-hosted add-in (allowing that code to run in Azure or some other web server). Since I wanted to create a SharePoint-hosted add-in, I had to find a way to accomplish this using only JavaScript. Thankfully, I found Chris O’Brien’s post with code showing how to provision files to the host web using JavaScript. You will see that my code is based heavily on the example he provides and provisions the following files from the add-in web to the host web:
- jQuery-1.9.1.min.js – jQuery
- HeaderFooter.js – our logic to read the header/footer configuration data from the host web property bag and render the header and footerelements
- StickyFooter.js – Randy Drisgill’s Sticky Footer implementation for SharePoint 2013 (with the one tweak described above)
Add-in part for setting configuration values
The add-in also includes an add-in part (deployed to and rendered from the add-in web) that allows users to enable/disable the header/footer and set the text and colors:
When the add-in part loads, it uses JavaScript to query the property bag of the host web to see if these settings already exist and if so, prepopulates the values in the form. Once the user has made the necessary changes, clicking Set Values will save the changes back to the property bag (again, using JavaScript).
NOTE: The need for our add-in to read and write data to and from the host web property bag requires us to request the FullControl permission at the Web scope in our add-in manifest (AppManifest.xml):
What about MDS?
SharePoint 2013 introduced the Minimal Download Strategy (MDS), which reduces page load time by sending only the differences when users navigate to a new page. While this is a wonderful benefit, it wreaks havoc with solutions that need to manipulate the DOM every time a new page is loaded. In other words, our header and footer may render perfectly on the home page when we first load it, but thanks to MDS when we navigate to the “Documents” library, only part of the page will actually be re-rendered. Our header and footer will not display properly (if at all) when only part of the page is re-rendered to support a new page request.
For much deeper reading on this subject, I encourage you to go through Wictor Wilen’s blog posts introducing the MDS and explaining the correct way to execute JavaScript functions on MDS-enabled sites. My code is based on Wictor’s solution and works properly in scenarios where MDS is enabled and where it is not (e.g., on Publishing sites or any site where the MDS feature has been deactivated).
We handle MDS by calling RegisterModuleInit() with the function we need to execute on every page load in HeaderFooter.js:
if (typeof _spPageContextInfo != "undefined" && _spPageContextInfo != null) { // MDS enabled RegisterModuleInit(_spPageContextInfo.siteServerRelativeUrl + 'Style Library/Headerfooter.js', DJ.HeaderFooter.Render.initializeHeaderFooter); } // Run now on this page (and non-MDS scenarios) DJ.HeaderFooter.Render.initializeHeaderFooter();
The code
I have posted the code for this add-in to GitHub at the following location:
https://github.com/dannyjessee/SiteHeaderFooter
I encourage you to download it, try it out in your environment, and let me know if you run into any issues with it. My sincere thanks go out to Chris O’Brien, Randy Drisgill, and Wictor Wilen for giving me the building blocks needed to put this add-in together.
Getting “Unexpected response data from server” error using SharePoint JSOM?
I was clearing the dust off an old demo for my session on cloud-hosted apps at SharePointFest DC next month when I ran into a problem. As I walked through the demo (which I had given three times before at SPS Events in Boston, Baltimore, and DC), I was getting an error when testing my ability to query for list data in my cloud-hosted app’s app web in my Office 365 developer tenant using JSOM:Knowing this script had worked for months in the past, I turned to the Internet. Sure enough, others appeared to be experiencing the same problem. One app developer even worked around the problem by abandoning the use of JSOM altogether in favor of the REST APIs.
What’s the problem here?
In a cloud-hosted app, the app’s business logic is deployed to a remote web (separate from its app web, which resides in SharePoint). This remote web is likely on a different domain (and is definitely on a different domain if you are using SharePoint Online). Querying for data that resides in the app web from a script running in your remote web requires the use of cross-domain JavaScript calls, which are generally blocked for security reasons. To work around this requires the use of the JavaScript Cross-Domain Library (sp.requestexecutor.js).
My demo script looked like this:
var clientContext = new SP.ClientContext(appweburl); var appWeb = clientContext.get_web(); appWebList = appWeb.get_lists().getByTitle(listname); var camlQuery = new SP.CamlQuery(); camlQuery.set_viewXml("
"); appWebListItems = appWebList.getItems(camlQuery); clientContext.load(appWebList); clientContext.load(appWebListItems); clientContext.executeQueryAsync(onAppWebGetListDataSuccess, onJSOMError);50 Once I initialized the ProxyWebRequestExecutorFactory object and set it as the factory of my ClientContext object, everything worked as expected. (NOTE: The script in my demo that queried for data in the host web was already making proper use of the cross-domain library.)
Here is the proper script to initialize the ClientContext and associate the appropriate ProxyWebRequestExecutorFactory with it:
var clientContext = new SP.ClientContext(appweburl); var factory = new SP.ProxyWebRequestExecutorFactory(appweburl); clientContext.set_webRequestExecutorFactory(factory); var appWeb = clientContext.get_web(); // Remaining code is the same as above
Conclusion
I’m not sure exactly what changed that would cause this problem to start occurring. Common sense dictates that the ProxyWebRequestExecutorFactory should be needed for all cross-domain calls, whether they be to the host web or the app web. I’m less concerned about that than why the old code ever worked (that did not make use of it). Nonetheless, if you find yourself in a situation where a script that definitely worked in the past now fails with an “Unexpected response data from server” error when using the JSOM, ensure you are making proper use of the cross-domain library as well.
Enabling custom ribbon buttons dynamically based on multiple selected item values using RefreshCommandUI and JSOM
Updated 9/6/2019: Thanks to Julien for noticing that the CountDictionary() function no longer exists in SharePoint 2016 and beyond. Since SP.ListOperation.Selection.getSelectedItems(context) already returns the collection of selected items, I replaced the call to CountDictionary() with selectedItems.length instead.
Updated 12/23/2014: Thanks to Chris Bell for pointing out some errors in the JavaScript in this post. The script and sample code below have been updated to reflect these corrections.
In this post, I will show you how to use the built-in RefreshCommandUI() method to update the enabled status of a custom ribbon button based on field values in multiple selected list items. In my searching, I have only found examples that changed the enabled status of a custom ribbon button based on a single selected item (I even blogged a very simple example of this here).
For an exhaustive treatment of how to add a custom button to the server ribbon, check out Walkthrough: Adding a Button to the Server Ribbon on MSDN. Our focus in this post is on the CommandUIHandler element of the XML necessary to declare this button, which contains an EnabledScript attribute that contains JavaScript that must return true or false–true if the button should be enabled, false to disable it.
In this example, we set our EnabledScript to “javascript:EnableFinishedButton();” which enables us to declare this function in an external JavaScript file (that we can load via a ScriptLink Custom Action). As an added bonus, this makes our script MUCH easier to locate and debug using our browser’s built-in JavaScript debugging tools! In this example, our EnableFinishedButton function will return true to enable our “Mark As Finished” ribbon button ONLY when ALL selected items in the list (one or more) have a value of “In Progress” in the “Status” field. This example should be easy to adapt to suit your needs.
For completeness, here is the XML necessary to declare the button and associated CommandUIHandler, as well as load the external JavaScript file containing the custom functions we need. While custom ribbon buttons can be deployed in SharePoint-hosted apps, I have packaged this as a farm solution so that I may specify JavaScript functions defined in an external file (loaded via a ScriptLink custom action) for the CommandAction and EnabledScript attributes. As an aside, I strongly encourage you to read Sean McDonough’s post on Custom Ribbon Button Image Limitations with SharePoint 2013 Apps to learn about some interesting (and frustrating!) limitations that forced me to go the farm solution route.
Here are the contents of CustomAction.js, which I am provisioning to the LAYOUTS folder:
function EnableFinishedButton() { var context = SP.ClientContext.get_current(); var list; var selectedItems = SP.ListOperation.Selection.getSelectedItems(context); var totalSelectedItems = selectedItems.length; if (totalSelectedItems > 0) { var web = context.get_web(); context.load(web); var listId = SP.ListOperation.Selection.getSelectedList(); list = web.get_lists().getById(listId); // We will use this variable to determine whether EnableFinishedButton() is being called directly or by RefreshCommandUI() var hadToMakeCall = false; if (typeof this.itemRows == "undefined" || this.itemRows.length != totalSelectedItems) { // This will be true if this is the first time an item has been selected in the list OR if the selected items have changed, forcing the need to check again hadToMakeCall = true; GetItemsStatus(); } // If we just issued the async call, do not enable the button yet if (hadToMakeCall) { return false; } else { // Once the call has returned, set the enabled status based on the returned value return this._can_be_enabled; } } else { this.itemRows = undefined; return false; } function GetItemsStatus() { // Store the selected list items in an array where their values can be checked itemRows = []; for (i in selectedItems) { itemRows[i] = list.getItemById(selectedItems[i].id); context.load(itemRows[i]); } context.executeQueryAsync(Function.createDelegate(this, onGetItemsSuccess), Function.createDelegate(this, onGetItemsQueryFailed)); } function onGetItemsSuccess() { this._can_be_enabled = true; // Iterate through all selected items. If one is false, the value of _can_be_enabled will be false and the button will not be enabled for (i in itemRows) { this._can_be_enabled = this._can_be_enabled && itemRows[i].get_item("Status") == "In Progress"; } // Now we can call the EnabledScript function again RefreshCommandUI(); } function onGetItemsQueryFailed(sender, args) { alert(args.get_message()); } } function MarkItemsFinished() { var context = SP.ClientContext.get_current(); var web = context.get_web(); var selectedItems = SP.ListOperation.Selection.getSelectedItems(context); var listId = SP.ListOperation.Selection.getSelectedList(); var list = web.get_lists().getById(listId); var i; for (i in selectedItems) { // Update the "Status" field of each selected item to have a value of "Finished" var listItem = list.getItemById(selectedItems[i].id); listItem.set_item("Status", "Finished"); listItem.update(); } context.executeQueryAsync(Function.createDelegate(this, onUpdateItemsSuccess), Function.createDelegate(this, onUpdateItemsFailed)); function onUpdateItemsSuccess() { alert("Items updated!"); } function onUpdateItemsFailed() { alert(args.get_message()); } }
Once deployed, we navigate to our custom list. As long as the items we select have a value of “In Progress” in the “Status” field, the custom button remains enabled:
As soon as we select an item that does not have a value of “In Progress” in the “Status” field, the button is no longer enabled:
Download the sample code for this project:
Using JSOM to write (small) files to a SharePoint 2013 document library
Update 8/24/2017: The code for this app is available for download here.
Update 3/3/2013: Problem solved! The culprit: “Protected View” in Office 2013. Thanks to this blog post from Tobias Lekman, after disabling protected view in each of the Office 2013 applications (I had to update these settings separately in Word, Excel, and PowerPoint), every document I uploaded using the code below opened without any errors on my CloudShare VM.
Update 2/16/2013: The issue I had opening files uploaded into SharePoint described below may not be an issue after all. As it turns out, the CloudShare VM where I was doing my development and testing is generating the same error for ANY document I upload, even “the old fashioned way.” I accessed the same document library from a remote client and the same products that generated errors when being uploaded using the code above opened just fine, without any issues or errors. I will update this post as I learn more about the root cause of this problem.
A recent post on Yammer lamented the lack of examples in the SharePoint 2013 API documentation that use the JavaScript Object Model (JSOM) to do anything more than create a very basic text file in a SharePoint document library.
After lots of digging and a fair amount of trial and error, I now understand why that is the case.
The use case seems simple enough: allow the user to select a file from his or her local machine using an HTML DOM FileUpload object on a form, then use JSOM to upload this file into a document library. It’s certainly easy enough to do using the Client Script Object Model (CSOM). As it turns out, there are a couple of very good reasons why your document upload capability (whether you package it into an app for SharePoint or something else) should NOT leverage JSOM:
- Per MSDN, you can only work with files up to 1.5 MB when using JSOM. It is recommended that you use REST to deal with larger files. (Incidentally, there is a comment in the article on using the REST endpoints that reads “See How to: Complete basic operations using JavaScript library code in SharePoint 2013 for a code example that shows you how to upload a binary file that is smaller than 1.5 MB by using the SharePoint 2013 Javascript object model.” Unfortunately, the only code example in that article creates a very rudimentary plain text file.)
- Unless your browser supports the File APIs introduced in HTML5 (specifically the FileReader API), you are out of luck. As a general rule, browsers will block attempts by JavaScript to access and read files from the local file system for security reasons. If you are using IE, only version 10 supports the FileReader API.
Although I was somewhat discouraged by this news, I was determined to develop an app for SharePoint 2013 that presented a simple file upload control to the user and stored the file in a document library (as long as it was smaller than 1.5 MB, of course). I figured as long as I could save Office documents to the library (i.e., more than a simple plain text file), I would have succeeded.
To accomplish this, I knew I would need to make use of the HTML5 FileReader API. (Because of that, I also knew I would need to test this solution using IE 10, Firefox, or Chrome!) Based on the MSDN documentation, I knew I would be setting the contents of the file by using a new SP.Base64EncodedByteArray. The FileReader API exposes three methods for reading the contents of a file:
- readAsText() – this method reads the plain text contents of a file, but does not properly handle binary files.
- readAsArrayBuffer() – this seemed to be the most promising option, but no matter how I tried to cast the contents of the ArrayBuffer to a Base64-encoded byte array, I was not able to successfully reproduce a file from the file system in a document library. If anyone out there has any suggestions that might enable readAsArrayBuffer() to work, please let me know in the comments!
- readAsDataURL() – this method returns the contents of the file using the Data URI scheme. Thanks to a handy utility method I found here, I can convert this Base64-encoded string into a JavaScript Uint8Array and use that to populate the SP.Base64EncodedByteArray that the JSOM expects.
Here is the JavaScript I ended up using:
$(document).ready(function () { // Get the URI decoded host web URL // We will use this to get a context here to write data hostweburl = decodeURIComponent(getQueryStringParameter("SPHostUrl")); }); function CreateFile() { // Ensure the HTML5 FileReader API is supported if (window.FileReader) { input = document.getElementById("fileinput"); if (input) { file = input.files[0]; fr = new FileReader(); fr.onload = receivedBinary; fr.readAsDataURL(file); } } else { alert("The HTML5 FileSystem APIs are not fully supported in this browser."); } } // Callback function for onload event of FileReader function receivedBinary() { // Get the ClientContext for the app web clientContext = new SP.ClientContext.get_current(); // Use the host web URL to get a parent context - this allows us to get data from the parent parentCtx = new SP.AppContextSite(clientContext, hostweburl); parentWeb = parentCtx.get_web(); parentList = parentWeb.get_lists().getByTitle("Documents"); fileCreateInfo = new SP.FileCreationInformation(); fileCreateInfo.set_url(file.name); fileCreateInfo.set_overwrite(true); fileCreateInfo.set_content(new SP.Base64EncodedByteArray()); // Read the binary contents of the base 64 data URL into a Uint8Array // Append the contents of this array to the SP.FileCreationInformation var arr = convertDataURIToBinary(this.result); for (var i = 0; i < arr.length; ++i) { fileCreateInfo.get_content().append(arr[i]); } // Upload the file to the root folder of the document library this.newFile = parentList.get_rootFolder().get_files().add(fileCreateInfo); clientContext.load(this.newFile); clientContext.executeQueryAsync(onSuccess, onFailure); } function onSuccess() { // File successfully uploaded alert("Success!"); } function onFailure() { // Error occurred alert("Request failed: " + arguments[1].get_message()); } // Utility function to remove base64 URL prefix and store base64-encoded string in a Uint8Array // Courtesy: https://gist.github.com/borismus/1032746 function convertDataURIToBinary(dataURI) { var BASE64_MARKER = ';base64,'; var base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length; var base64 = dataURI.substring(base64Index); var raw = window.atob(base64); var rawLength = raw.length; var array = new Uint8Array(new ArrayBuffer(rawLength)); for (i = 0; i < rawLength; i++) { array[i] = raw.charCodeAt(i); } return array; }
This code works!
–mostly.In my environment, Excel Services was able to successfully open my basic test spreadsheet in the browser:I could also use the Download a Copy option to save local copies and successfully open files of any type:
For a simple Word document, though, I was unable to click the link from the document library and have it open successfully in Word. Instead, Word reported an error when trying to open the document:If you receive the error shown below when opening a document from SharePoint, it is due to “Protected View” in Office 2013. To disable Protected View, follow the steps outlined here.
Regardless of your Office 2013 Protected View settings, the Download a Copy option will open the document without the annoying error message.
JavaScript to Conditionally Enable a SharePoint Custom Ribbon Button
During my session on getting started with apps for SharePoint 2013, I demonstrate how to use the “extension” app shape to define a UI custom action that displays a button in the ribbon. When clicked, this button allows the user to view a Bing traffic map centered at the location specified in the selected list item. This is done using a CustomAction in the same declarative manner as it was done in SharePoint 2010, using the following XML:
Installing the app adds a button labeled “Plot on Bing Traffic Map” to the “Items” tab on all custom lists in the host web where the app is installed:
In my CommandAction of the CommandUIHandler for the CommandUIExtension I define to create the button, I specify that clicking the button will take the user to the following URL:
~appWebUrl/Pages/BingTrafficMap.html?{StandardTokens}&ListID={ListId}&ItemID={SelectedItemId}
Note the use of the SelectedItemId token in the URL above. This means my BingTrafficMap.html page can rightly expect to have an ItemID parameter in its query string, which can then be parsed to get the list item and read its properties using JavaScript.
But what if the user hasn’t selected any items? Or multiple items? Since I can only center a map on one single location, I need to have more control over when this button is enabled. In this case, I only want the button to be enabled when exactly one item in the list is selected. (By default, any ribbon buttons you create through custom actions will always be enabled, regardless of whether or not any items are selected.)
As it turns out, there is an EnabledScript attribute of the CommandUIHandler that will enable or disable the button depending on whether the function returns true or false. I was able to adapt this code from Tomasz Rabiński to add the following logic to my CommandUIHandler:
Note that the function EnableDisable–which I can define and then call within the EnabledScript attribute–returns true only if the number of selected items equals 1.
Now my button behaves exactly as I would expect.
No items selected (disabled):
One item selected (enabled):
More than one item selected (disabled):
I have updated my demo code to include this change. You can download this code here.
- Our footer