Goodbye Ferrari, hello Napa?

Hey there!

If you’ve seen my series about the SharePoint DVWP in Ferrari mode (Part 1, Part 2, Part 3) you remember that we ended up with a small HTML5/CSS3/JavaScript app that is running on SP 2010 and SP 2013.

Today we take some of our lessons learned and try to apply them into the brave new world of SP 2013 apps or to be more precise into the world of Napa apps. Please note that I’m not going to cover the whole SharePoint 2013 App Model, but if you want to learn more about it, spend an hour and watch Jeremy Thake’s screencast about that topic.

Why should we talk about Napa apps in the first place? One reason is that Napa apps are the lowest hanging fruits in SP2013 development at least in terms of required infrastructure. You don’t need to setup an on premise SP2013 development environment and you’re not required to have Visual studio 2012 installed.

With that said if you want to follow along and haven’t Napa yet, this Office 365 Developer page will get you started.

Just to get our feet wet let’s create an OOTB app called ‘SharePointApp1’ and run the project without further modification.

Step 1 Add a new project Napa Development Step 2 Choose App for SharePoint and give it a name Napa Development Step 3 Press the Run project button and see how the system is doing its job to package, deploy and launch your app Napa Development Step 4 Be impressed with the results of all your hard work :) Napa Development

Take a quick glance at the HTML source of our impressive app. I say impressive because even though the payload is just the User Name it has 644 lines of code. The reason is that our small ‘payload’ is nicely embedded into SP2013, which comes with a price.

 1 <%-- The following 4 lines are ASP.NET directives needed when using SharePoint components --%>
 2 <%@ Page Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" MasterPageFile="~masterurl/default.master" language="C#" %>
 3 <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 4 <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 5 <%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 6 
 7 <%-- The markup and script in the following Content element will be placed in the <head> of the page --%>
 8 <asp:Content ContentPlaceHolderId="PlaceHolderAdditionalPageHead" runat="server">
 9     <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.2.min.js" type="text/javascript"></script>
10 
11     <!-- Add your CSS styles to the following file -->
12     <link rel="Stylesheet" type="text/css" href="../Content/App.css" />
13 
14     <!-- Add your JavaScript to the following file -->
15     <script type="text/javascript" src="../Scripts/App.js"></script>
16 </asp:Content>
17 
18 <%-- The markup and script in the following Content element will be placed in the <body> of the page --%>
19 <asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">
20 
21     <div>
22         <p id="message">
23             <!-- The following content will be replaced with the user name when you run the app - see App.js -->
24             initializing...
25         </p>
26     </div>
27 
28 </asp:Content>

Within the Source we see some familiar ASP.NET directives with a link to a masterpage and then two <asp:Content> blocks.The PlaceHolderAdditionalPageHead is used to load your app specific CSS and JavaScript. The CSS is of no interest for us at the moment so let’s take a look at App.js instead.

 1 var context;
 2 var web;
 3 var user;
 4 
 5 // This code runs when the DOM is ready. It ensures the SharePoint
 6 // script file sp.js is loaded and then executes sharePointReady()
 7 $(document).ready(function () {
 8     SP.SOD.executeFunc('sp.js', 'SP.ClientContext', sharePointReady);
 9 });
10 
11 // This function creates a context object which is needed to use the SharePoint object model
12 function sharePointReady() {
13     context = new SP.ClientContext.get_current();
14     web = context.get_web();
15 
16     getUserName();
17 }
18 
19 // This function prepares, loads, and then executes a SharePoint query to get the current users information
20 function getUserName() {
21     user = web.get_currentUser();
22     context.load(user);
23     context.executeQueryAsync(onGetUserNameSuccess, onGetUserNameFail);
24 }
25 
26 // This function is executed if the above call is successful
27 // It replaces the contents of the 'helloString' element with the user name
28 function onGetUserNameSuccess() {
29     $('#message').text('Hello ' + user.get_title());
30 }
31 
32 // This function is executed if the above call fails
33 function onGetUserNameFail(sender, args) {
34     alert('Failed to get user name. Error:' + args.get_message());
35 }

Beside the fact that the small code leaks more than half a dozen globals, which makes some of us laugh and some of us cry, we see that getUserName() retrieves the user information from the app web via CSOM and finally onGetUserNameSuccess() renders the Hello username message.

Checkpoint So far everything was nice and easy I’d say and you shouldn’t have an issue if you follow along.

Now let’s switch gears by first getting rid of the SharePoint chrome. Simply copy Source2 and replace your existing default.aspx content in Napa. Running the app will show that the HTML source2 for the page is down to 18 lines.

By glancing through Source2 you see that we are still using some ASP.NET directives, just the reference to the masterpage has gone. In addition the <asp:Content> blocks have been replaced by some standard HTML5. In Part 1 we stripped down the DVWP in a similar manner, so we might call this mode the Naparari :-).

Please note that at this time the JavaScript won’t work as some required CSOM files are loaded via the masterpage we just removed.

 1 <%-- The following 5 lines are ASP.NET directives needed when using SharePoint components --%>
 2 
 3 <%@ Page Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Language="C#" %>
 4 
 5 <%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 6 <%@ Import Namespace="Microsoft.SharePoint" %>
 7 <%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 8 <%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 9 
10 <!DOCTYPE HTML>
11 <html>
12 <head>
13     <title>
14         <SharePoint:ProjectProperty Property="Title" runat="server" />
15     </title>
16     <!-- Add your CSS styles to the following file -->
17     <link rel="Stylesheet" type="text/css" href="../Content/App.css" />
18 </head>
19 <body>
20     <div id="message">
21         <!-- The following content will be replaced with the user name when you run the app - see App.js -->
22         initializing...
23     </div>
24     <script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.0.min.js" type="text/javascript"></script>
25     <script type="text/javascript" src="../Scripts/App.js"></script>
26 </body>
27 </html>

Second let’s make some modification to app.js.

  1. Goal: In addition to the username we want to retrieve the web title from the host web
  2. Goal: Instead of adding the required CSOM files, we are switching to OData
  3. Goal: While on it let’s get rid of the globals ;-).

Napa apps like other SharePoint Apps enforces a clear separation between the host web, the web where you install your app, and the app web, the place where your code is actually running.

As an example take a look at my host web URL: https://spirit2013preview.sharepoint.com/sites/dev/

Whenever a Napa app is launched it runs at an app web URL similiar to

https://spirit2013preview-b0204c96e900db.sharepoint.com/sites/dev/GoodbyeFerrariHelloNapa/Pages/Default.aspx

You see that an app specific GUID like -b0204c96e900db is added to the host header, so for our JavaScript code that is running in the app web there’s NO way to access information directly in the host web due to cross site scripting restriction.

Let’s see how our goals can be accomplished in the JavaScript below, which is based on code from MSDN. If you’re not familiar with the cross-domain library SP.RequestExecutor.js please check it out first.

Goal 1 and 2: The answer to both goals live in the execCrossDomainRequest() function. As you can see we are executing a call against the local SPAppWebUrl /_api/SP.AppContextSite(@target)/ OData endpoint and passing in SPHostUrl as @target. Effectivily this will provide us access to the remote SPHostUrl/_api endpoint (through the cross-domain library).

Beside the cross-domain library specific you can now use the same OData syntax that you would use locally e.g. here /web?$expand=CurrentUser is used to retrieve web and at the same time CurrentUser information.

Goal 3: There’s more than one way to accomplish that, but to be close to the origal version let’s wrap our code in an Immediately-Invoked Function Expression IIFE, which you might know as self-executing anonymous function as well.

 1 (function () {
 2     var params = getParams(),
 3         scriptbase = params.SPHostUrl + "/_layouts/15/";
 4 
 5 
 6     // Load the js file and continue to the
 7     //   success event handler.
 8     $.getScript(scriptbase + "SP.RequestExecutor.js", execCrossDomainRequest);
 9 
10 
11     // Function to prepare and issue the request to get
12     //  SharePoint data.
13     function execCrossDomainRequest() {
14         var executor = new SP.RequestExecutor(params.SPAppWebUrl);
15 
16         // Issue the call against the host web.
17         // To get the title using REST we can hit the endpoint:
18         //      app_web_url/_api/SP.AppContextSite(@target)/web/title?@target='siteUrl'
19         // The response formats the data in the JSON format.
20         // The functions successHandler and errorHandler attend the
21         //      success and error events respectively.
22         executor.executeAsync(
23             {
24                 url:
25                     params.SPAppWebUrl +
26                     "/_api/SP.AppContextSite(@target)/web?$expand=CurrentUser&@target='" +
27                     params.SPHostUrl + "'",
28                 method: "GET",
29                 headers: { "Accept": "application/json; odata=verbose" },
30                 success: successHandler,
31                 error: errorHandler
32             }
33         );
34     }
35 
36     // Function to handle the success event.
37     // Prints the host web's title to the page.
38     function successHandler(data) {
39         var jsonObject = JSON.parse(data.body);
40         $('#message').html(jsonObject.d.CurrentUser.Title  +
41                     '<br/>host web: <b>' + jsonObject.d.Title + '</b>');
42     }
43 
44     // Function to handle the error event.
45     // Prints the error message to the page.
46     function errorHandler(data, errorCode, errorMessage) {
47         $('#message').html("Could not complete cross-domain call: " + errorMessage);
48     }
49 
50 
51     // Returning the params object
52     function getParams() {
53         var params = {};
54         location.search.split('?')[1].split('&').forEach(function (param) {
55             var key = param.split('=')[0],
56                 val = decodeURIComponent(param.split('=')[1]);
57             params[key] = val;
58         });
59         return params;
60     }
61 
62 })();

After all that hard work here’s the Punchline:

Running the example right away won’t work!

The reason is that we missed the most important configuration part for SP 2013 apps. Whenever your app requires acccessing information in the host web you have to configure the appropriate permissions. In addition whenever a site admin installs an app, they have to decide if they trust your app or not.

It will be interesting to see how this new concept (new to SharePoint, you’re probably familiar with it from mobile apps) will pay off in the real world.

For Napa apps there’s just one additional step required as developer, so let’s walk trough that process quickly.

Step 1 As a developer you have to explicitly ask for permission for your app Napa Development Step 2 As site admin you have to trust the app Napa Development Step 3 Finally the desired result: No chrome, user name and (Host)web title. Napa Development

That’s it for today and I’m leaving you with this example, which -while visually not as impressive as the first version- under the hood is clean as hell and hopefully well understood ;-). Take it a starting point for your own Napa apps and let me know how it goes.

One question though: How do you feel about SP apps and the SP marketplace in general? Are you already sold (can’t wait for it to open, so that you can get rich) or do you have some concerns compared to traditional farm or sandbox solutions?

Published: August 24 2012

blog comments powered by Disqus