Tuesday, February 5, 2013

SharePoint 2013 meet Knockoutjs

Introduction
In these days I have been starting to write some application with Knockoutjs to understand how much I can use it on my SharePoint projects. With the new model of the SharePoint’s app, use Knockoutjs in an app, is quite similar to use it in a MVC project.
For this post I made a immersive full page app with the following SharePoint item:
  • App for SharePoint 2013 hosted in my SharePoint
  • One Content Type
  • One list that use the content type of above, with some static demo data

A quick introduction to the app catalog, app model and publish an app
<<In its most basic form, an app for SharePoint is a web application that is registered with SharePoint using an app manifest. An app manifest is an XML file that declares the basic properties of the app along with where the app will run and what to do when the app is started.>> (msdn par: What is an app for SharePoint?).
You have 3 options for hosting your apps in SharePoint:

Options for hosting apps for SharePoint

Those can be combined.
When you use the SharePoint-hosted & autohosted options you don't need to register your app and everything is included in the .app paclage.

Once the app pkg has been published, it is available for users to install. In my case (SharePoint-hosted) the pkg contains all the resources required to deploy the app during the installation process.
You publish the app by uploading it to a special type of site known as an app catalog site. This is the option to use when you want to make the app available to a specific on-premises farm.
This site con a special type of document library that is used to upload and store app package files.
This is mine:

App catalog web site


When you start to dev an app you have to decide which APIs to use. This choice is based on the kind of app tat you create, as a best practice use client APIs. But, not all server-side functionality is available from client APIs.


As you can see the Server OM APIs are related only with PowerShell and Timers job.
The new app model doesn't support running server-side code within the SharePoint host environment, so we cannot use the server-side API in apps, but we can use the client-side API.

For this example I used a App for SharePoint 2013:

SharePoint-hosted:

When you want publish your app you should only "deploy" it:


Pay attention, in the URL property of your project. It is the URL of the site to use for debugging! I have: https://dev.content.code/
Maybe this image can help you to understand:
SharePoint component isolation


My App
Very quickly following is the ingredients of my app.
project schema

As you can see I have:

this new content type that I use to save my information
my video content type

a list that use my content type
video list instance

To have the App ready to go, I had inserted data in my list instance before the deploy:
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <ListInstance Title="VideoInfoList" OnQuickLaunch="TRUE" TemplateType="10000" Url="Lists/VideoInfoList" Description="My List Instance">
        <Data>
            <Rows>
                <Row>
                    <Field Name="Title">The Shawshank Redemption</Field>
                    <Field Name="DateCompleted">1994-12-30T21:00:00Z</Field>
                    <Field Name="AverageRating">5</Field>
                </Row>
                <Row>
                    <Field Name="Title">The Godfather</Field>
                    <Field Name="DateCompleted">1972-12-30T21:00:00Z</Field>
                    <Field Name="AverageRating">5</Field>
                </Row>
                <Row>
                    <Field Name="Title">Pulp Fiction</Field>
                    <Field Name="DateCompleted">1994-12-30T21:00:00Z</Field>
                    <Field Name="AverageRating">4</Field>
                </Row>
                <Row>
                    <Field Name="Title">The Dark Knight</Field>
                    <Field Name="DateCompleted">2008-12-30T21:00:00Z</Field>
                    <Field Name="AverageRating">4</Field>
                </Row>
                <Row>
                    <Field Name="Title">Star Wars: Episode V - The Empire Strikes Back</Field>
                    <Field Name="DateCompleted">1980-12-30T21:00:00Z</Field>
                    <Field Name="AverageRating">4</Field>
                </Row>
                <Row>
                    <Field Name="Title">Matrix</Field>
                    <Field Name="DateCompleted">1999-12-30T21:00:00Z</Field>
                    <Field Name="AverageRating">4</Field>
                </Row>
                <Row>
                    <Field Name="Title">Jurassic Park</Field>
                    <Field Name="DateCompleted">1993-12-30T21:00:00Z</Field>
                    <Field Name="AverageRating">3</Field>
                </Row>
                <Row>
                    <Field Name="Title">A Fistful of Dollars</Field>
                    <Field Name="DateCompleted">1964-12-30T21:00:00Z</Field>
                    <Field Name="AverageRating">3</Field>
                </Row>
            </Rows>
        </Data>
    </ListInstance>
</Elements>

To use Knockoutjs you should add it in your project. You should use NuGet version: http://nuget.org/packages/knockoutjs

App information
If you are familiar with the Facebook’s app developing the following information will be obvious for you. As I wrote before, every apps has an AppManifest.xml in this file you can find some information like this:
<?xml version="1.0" encoding="utf-8" ?>
<!--Created:cb85b80c-f585-40ff-8bfc-12ff4d0e34a9-->
<App xmlns="http://schemas.microsoft.com/sharepoint/2012/app/manifest"
     Name="SDFVideos"
     ProductID="{07ff4877-f4cc-465b-bf16-cec326c22b81}"
     Version="1.0.0.0"
     SharePointMinVersion="15.0.0.0">
  <Properties>
    <Title>SDF.Videos</Title>
    <StartPage>~appWebUrl/Pages/Default.aspx?{StandardTokens}</StartPage>
  </Properties>

  <AppPrincipal>
    <Internal />
  </AppPrincipal>
  <AppPermissionRequests>
  </AppPermissionRequests>
</App>


If you open the AppManifest.xml through Visual Studio 2012 a window like this will be show:

appmanifest

You can see 5 tabs:
  • General

    • Title: The title of the SharePoint App.
    • Name: The name of the SharePoint App. Name is not localized, and is used in URLs that address App artifacts.
    • Version: The app version.
    • Icon: Specifics the URL of the image that is used to present the App.
    • Start page: The URL of the page visited when the SharePoint App is launched.
    • Query string: Specifics the query string to be added to the start page URL.

permissions


  • Permissions

    • Like the Facebook’s app, the SharePoint apps needs to have the permission set if those needs to use resources; for ex: write access to profiles, read access to site collection, etc…
      in this example you don’t need to provide some permissions because we haven’t code behind. It’s become useful when you, for example, develop an app that access to resources.

  • Prerequisites

    • is useful to specify the features and capabilities that your app depends on, like: Managed Metadata Web Service, Search Service, Secure Store Services, User Profile Service, etc
  • Supported Locales

    • List of language culture combination that this app supports
  • Remote Endpoints

    • You can use this page to set Remote Endpoints useful for your app

The CORE: Knockout
Now we can talk about the Knockoutjs experience. As you see in the picture about my project, I have a Default.aspx page into the Page folder. This is the most important markup lines of my page:
<%-- The markup and script in the following Content element will be placed in the <head> of the page --%>
<asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
    <script type="text/javascript" src="../Scripts/jquery-1.7.1.min.js"></script>
    <script type="text/javascript" src="/_layouts/15/sp.runtime.debug.js"></script>
    <script type="text/javascript" src="/_layouts/15/sp.debug.js"></script>
    <script type="text/javascript" src="../Scripts/knockout-2.2.1.js"></script>

    <!-- Add your CSS styles to the following file -->
    <link rel="Stylesheet" type="text/css" href="../Content/App.css" />

    <!-- Add your JavaScript to the following file -->
    <script type="text/javascript" src="../Scripts/App.js" defer="defer"></script>

    <script type="text/javascript">
        $(document).ready(function () {
            ko.applyBindings(SDF.Views.DefaultPage);
        });
    </script>
    
</asp:Content>
<%-- The markup and script in the following Content element will be placed in the <body> of the page --%><asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
    <button data-bind="click: loadItems">Load</button>
    <br />
    <br />
    <ul data-bind="foreach: items">
        <li><label data-bind="text: Title"></label> - <label data-bind="text: AverageRating"></label> - <label data-bind="text: DateCompleted"></label></li>
        <pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
    </ul>
</asp:Content>


Obviously the app needs a reference to our knockout-2.2.1.js, next you have a reference to the App.js where all the JavaScript code take place, and 2 lines of jQuery code that execute the knockout binding. The applyBindings function is the method use by Knockout to activate the binding, in this example, to the DefaultPage view (this function is in the App.js file). Next you can see some html markup lines. The data-bind attribute isn’t native to HTML but it is perfectly OK, but since the browser doesn’t know what it means, knockout does well but you need to active it by the applyBindings method.
The foreach method duplicates a section of markup for each item in the items array, this is especially useful for rendering lists or tables. The text binding apply the text found in the current item to the DOM element associated instead the click binding adds an event handler to a JavaScript function that I wrote inside my DefaultPage.

Knockout: the App.js file
Inside that file you can find the MVVM structure used by Knockout to works with the Default.aspx page. The first method I want show you is just an utility method:
SDF.Utility = (function () {
    function QueryStringParser(paramToRetrieve) {
        var params, i, singleParam;

        params = window.document.URL.split("?")[1].split("&");
        for (i = 0; i < params.length; i = i + 1) {
            singleParam = params[i].split("=");
            if (singleParam[0] === paramToRetrieve) {
                return singleParam[1];
            }
        }

        return "";
    }

    return { getQueryStringParameter: QueryStringParser };
}());


The getQueryStringParameter is useful when you needs to get value from the query string. At least you need it to get two variables:
  • SPHostUrl=https://dev.content.code
  • SPAppWebUrl=https://App-5a060d47f59262/apps/code/SDFVideos

        Those two variables are useful because tell us: SPHostUrl, where the app is hosted; SPAppWebUrl, where is into the Apps Catalog. You can use those two variables to, for ex, access to the SPSite properties by JavaScript or know the REST address.

This is our view model:
SDF.Views = SDF.Views || {
  DefaultPage: (function () {

      function Item(data) {
          var self = this;
          self.Title = data.Title;
          self.AverageRating = ko.observable(data.AverageRating);
          self.DateCompleted = data.DateCompleted;
      }

      var appweburl = decodeURIComponent(SDF.Utility.getQueryStringParameter("SPAppWebUrl"));

      var doSuccess = function (items) {
        var buffer;

        buffer = $.map(items.d.results, function (item) { return new Item(item); });

        viewModel.items(buffer);
    };

      var doError = function () {
          alert("error");
      };
    
      var viewModel = {
          items: ko.observableArray([]),

          loadItems: function () {
              jQuery.ajax({
                  url: appweburl + "/_api/web/lists/GetByTitle('VideoInfoList')/items",
                  headers: { "Accept": "application/json; odata=verbose" },
                  success: doSuccess,
                  error: doError
              });
          },
      };

      return viewModel;
  }())
};

This is a very small and easy view model. The function Item identify our object in the items array. When you click the button Load the method loadItems is fire, the function associated will make a REST call to our List in SharePoint. If the call will end successful the method doSuccess will be call otherwise the method doError. The doSuccess method is the most interesting of the two. By the jQuery map method you can easily translate the JSON result in a new array of Item objects. When the array is ready you apply the change to the items object. In this case the items array change is status from a empty array to an array whit some elements. Knockout will know about this change because the items object isn’t a standard array, indeed it is an ko.observableArray. You have to use this object if you want to detect and respond to changes of a collection of things. This method tracks which objects are in the array, not the state of those objects.

If you deploy the app the result will be this:
  • before the click

2-1-2013 12-05-08 PM
  • after the click

2-1-2013 12-06-37 PM


Knockout: debug info
A very useful thing that I didn't explain before is this line:
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
that is inside the foreach binding in the Default.aspx. That is a way that I use to debug my Knockout page to understand if the variable are bound well.

Knockout: performance issues
Let me tell you a last thing just to close well the post :). During my test I got the following performance problem:

knockout performance issues

As you can see knockoutjs got, in total, 23 secs to repaint all the page for a request of 100 items. Why? The page is very easy, that’s a piece of my page:
<tbody data-bind="foreach: filteredItems">
  <tr>
    <td>
      <!-- ko ifnot: edit -->
      <label  data-bind="text: name"></label>
      <!-- /ko -->
      <!-- ko if: edit -->
      <input type="text" data-bind="value: name" />
      <!-- /ko -->
    </td>
    <td>
      <!-- ko ifnot: edit -->
      <label data-bind="text: color"></label>
      <!-- /ko -->
      <!-- ko if: edit -->
      <select data-bind="options: $root.colors, value: color, optionsCaption: 'Choose...', enable: edit">
      </select>
      <!-- /ko -->
    </td>
    <td>
      <!-- ko ifnot: edit -->
      <label data-bind="text: model"></label>
      <!-- /ko -->
      <!-- ko if: edit -->
      <select data-bind="options: $root.productModels, value: model, optionsCaption: 'Choose...', enable: edit">
      </select>
      <!-- /ko -->
    </td>


Ok, it is not all the markup, but the rest of the page is not fascinating. The main differences with the page of before are the two select with the binding:
$root.productModels
$root.colors
the keyword $root is a pointer to the ViewModel object. Why you need to use it? Because you are inside the foreach binding but the colors and the productModels are properties of the ViewModel:
SDF.Views = SDF.Views || {
    IndexViewModel: function () {
        var self;
        self = this;

        self.items = ko.observableArray([]);
        self.colors = ko.observableArray([]);
        self.productModels = ko.observableArray([]);

But why knockoutjs has need 23 secs to repaint my page?
The problems are the two select because forearch item it needs to recreate the select tag and rebinding it.
But I want the two select because the user should have the ability to edit an item and change the properties. To accomplish with this task and have better performance the solution is: use the if binding.
The if binding causes a section of markup to appear in your document, it’s difference than his brother visible set to false, because with the visible attribute I only set if the data in visible or not even though it is in the DOM, otherwise the if binding doesn’t write the data inside the DOM.
This is what my page looks like know:
<tbody data-bind="foreach: filteredItems">
  <tr>
    <td>
      <!-- ko ifnot: edit –>
      <label  data-bind="text: name"></label>
      <!-- /ko -->
      <!-- ko if: edit -->
      <input type="text" data-bind="value: name" />
      <!-- /ko -->
    </td>
    <td>
      <!-- ko ifnot: edit -->
      <label data-bind="text: color"></label>
      <!-- /ko -->
      <!-- ko if: edit -->
      <select data-bind="options: $root.colors, value: color, optionsCaption: 'Choose...', enable: edit"></select>
      <!-- /ko -->
    </td>
    <td>
      <!-- ko ifnot: edit -->
      <label data-bind="text: model"></label>
      <!-- /ko -->
      <!-- ko if: edit -->
      <select data-bind="options: $root.productModels, value: model, optionsCaption: 'Choose...', enable: edit"></select>      <!-- /ko -->
    </td>

With this solution if you ask for 100 items the result is:

2-1-2013 3-25-48 PM


and you still edit your item.
In the next post I will show you how edit an item.

No comments:

Post a Comment