Attaching a file to a list item in SharePoint add-in model

Long time since I wrote about the SharePoint. Is still my job, I am still working on this, but I simply did not find the time to discover new things. Now, as SharePoint add-in model is more and more adopted in SharePoint development, due to the migration of large companies to Office 365, I am starting to face new challenges. One of this is how to attach a file to an existing list item.

<input type="file" id="attachment1" />

As HTML5 is the new standard for web, file control is able to read the content of a file as a binary string, so no more needed to server side languages. For rest, jQuery and SharePoint JavaScript libraries will do the work for you.


       Type.registerNamespace('Shp');

       Shp.Attachments = function () {
            /// <summary>Shp Attachments static class</summary>
            throw 'Cannot initiate Shp.Attachments static class';
        };

        Shp.Attachments.get_file = function (fileInput) {
            /// <summary>This method is used to get the content of the file as binary string</summmary>

            var deffered = jQuery.Deferred();
            var reader = new FileReader();
            reader.onload = function (e) { 
                deffered.resolve(e.target.result);
            };  

            reader.onerror = function (e) {
                deffered.reject(e.target.error);
            };

            reader.readAsBinaryString(fileInput.files[0]);
            return deffered.promise();
        };


        Shp.Attachments.add = function (listName, itemId, fileInput, webUrl, success, fail) {
            /// <summary>Add attachments</summary>
            /// <param>List name</param>
            /// <param>Item Id</param>
            /// <param>File input controls</param>
            /// <param>Web url</param>
            var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, optional: false, mayBeNull: false },
                                                            { name: 'itemId', type: String, optional: false, mayBeNull: false },
                                                            { name: 'filesInput', type: HTMLElement, optional: false, mayBeNull: false },
                                                            { name: 'webUrl', type: String, optional: false, mayBeNull: true },
                                                            { name: 'success', type: Function, optional: false, mayBeNull: false },
                                                            { name: 'fail', type: Function, optional: true, mayBeNull: false }], true);
            if (e) throw e;


            var webUrl = webUrl || _spPageContextInfo.webAbsoluteUrl;
            var fail = fail || function (err) { alert(err); };

            Shp.Attachments.get_file(fileInput).then(function (fileContent) {
                var parts = fileInput.value.split("\\");
                var fileName = parts[parts.length - 1];
                // Attachments add internal method
                Shp.Attachments._add(listName, itemId, fileContent, fileName, webUrl, success, fail);
            });
        };

        Shp.Attachments._add = function (listName, itemId, fileContent, fileName, webUrl, success, fail) {

            var scriptBase = webUrl + "/_layouts/15/";
            jQuery.getScript(scriptBase + "SP.RequestExecutor.js", function () {

                var executor = new SP.RequestExecutor(webUrl);
                executor.executeAsync({
                    url: webUrl + "/_api/web/lists/GetByTitle('" + listName + "')/items(" + itemId + ")/AttachmentFiles/add(FileName='" + fileName + "')",
                    method: "POST",
                    binaryStringRequestBody: true,
                    body: fileContent,
                    state: "Update",
                    success: function () {
                        success(itemId);
                    },
                    fail: function (data) {
                        fail(data.responseText);
                    }

                });

            });
        };

        Shp.Attachments.registerClass('Shp.Attachments');

Once code is organized, attaching a file become an easy task.

 Shp.Attachments.add('Tickets', '2', document.getElementById('attachment1'), null, function (itemId) {
        alert(itemId);
    }, function (err) {
        alert('Error: ' + err);
    });

Shp.Attachments.add accepts following parameters:

  • List name as string
  • Item id as string
  • HTML file control as HTML element
  • Web URL. It can be nul and in this case is used _spPageContextInfo.webAbsoluteUrl
  • Success method to be executed if attachment is added
  • Fail method to be executed if attachment failed

Copy, adapt the code according to your needs and use it. Happy coding! If it helps, you can just say hi to me and I will be happy as well. 🙂

Advertisements

Adding Chrome to browser identification list for Microsoft Ajax

Microsoft Ajax library can identify browser type and version for Firefox, Internet Explorer and Safari. Chrome is missing from this list and it seems Microsoft added Chrome to “init.js” but did not extend Microsoft Ajax. However, as Microsoft Ajax is not limited to SharePoint, you can add Chrome by adding the following code in another JavaScript file (be sure you load the file after Microsoft Ajax is loaded).

(function () {

    function execute() {
        Sys.Browser.Chrome = {};
        var a = navigator.userAgent.toLowerCase();
        if (a.toLowerCase().indexOf("chrome") != -1) {
            Sys.Browser.agent = Sys.Browser.Chrome;
            Sys.Browser.version = parseInt(a.substring(a.indexOf("chrome/") + 7));
        }
    }

    if (typeof (Sys) !== "undefined" && typeof (Sys.Browser) !== "undefined") {
        execute();
    }

})();

And now you can easily identify is browser is Chrome and what version is used.

// Check if browser is Chrome
if (Sys.Browser.agent === Sys.Browser.Chrome) {
    // Alert current Chrome version
    alert(Sys.Browser.agent);
}

Calculate working hours between two days with JavaScript

Calculating working hours between two days with JavaScript is a regular requirement in large companies. In general, managers want to measure clean performance of the employees, excluding weekends and what is outside the business hours (in my case 9 AM to 6 PM). The problem appears when you cannot use C# or Java or any other language of this type and you are limited to JavaScript. JavaScript cannot offer an elegant solution, but is still offering the functionality.

/// <Reference Name="MicrosoftAjax.js" />

function getBusinessHoursDiff(start, end) {
    /// <summary>Calculate business hours between two dates</summary>
    /// <param name="start" type="Date" maybNeNull="false" optional="false">Start date</param>
    /// <param name="end" type="Date" maybNeNull="false" optional="false">End date</param>
    var e = Function.validateParameters(arguments, [{ name: 'start', type: Date, maybNeNull: false, optional: false },
                                                    { name: 'end', type: Date, maybNeNull: false, optional: false }, ], true);

    // Passing incorrect parameters type will result in returning an empty string, you can change the logic as you wish here
    if (e) return '';

    // Define variables and clone the provides date, we do not want to modify them outside as are passed as reference
    var startDate = new Date(startDate.valueOf());
    var endDate = new Date(endDate.valueOf());
    var count = 0;

    for (var i = start.valueOf() ; i < end.valueOf() ; i = (start.setMinutes(start.getMinutes() + 1)).valueOf()) {
        if (start.getDay() != 0 && start.getDay() != 6 && start.getHours() >= 9 && start.getHours() < 18) {
            count++;
        }
    }

    return count / 60;
}

Function is simple, even to be honest I did find it after almost two days. Is also not perfect when it comes to performance as is calculating the different till seconds. Still, if you do not want to be so exact, you can calculate till hours level (setMinutes method with set setHours). I am not sure how suitable is this solution for you, but is for sure something you can start from.

Date type extensions in Microsoft Ajax library

I have to admit, Microsoft Ajax JavaScript library is not so well known and popular these days. But still if offers a lot of functionalities a developer might use doing some client side work. I believe can be very useful for SharePoint developers, as SharePoint, being a web forms based technology, use it.

For this article, my objective is to explain a little bit more about Ajax Date type extensions, which are a set of methods designed to extend JavaScript built-in Date object.

Format date as string

Converting a date object to a formatted string has never been easier. Ajax is adding a extension method to Date object called “format” which accepts a format string as parameter. As rule this string should contain the following placeholders to show different parts of the date:

  • yyyy – show full year
  • yy – show short format of year (only the last 2 numbers)
  • M – show short month number (doesn’t add leading zero if month number is under 10)
  • MM – show month
  • MMM – show month name, like Oct, Jan
  • MMM – show full month name, like October and January
  • d – show short day number (doesn’t add leading zero if day number is under 10)
  • dd – show day
  • ddd – show day name, like Sat, Sun….
  • dddd – show full day name, like Saturday or Sunday
  • hh- show hours
  • mm – show minutes
  • ss- show seconds
var dt = new Date();

// 2015-10-22T09:18:30Z
dt.format("yyyy-MM-ddThh:mm:ttZ") 

// 2015/10/22
dt.format("yyyy/MM/dd");

// 22-Oct-15
dt.format("dd-MMM-yy");

// Output Thu
dt.format("ddd");

// Output Thursday
dt.format("dddd");

// Output October
dt.format("MMMM");

You can play with this format string parameter and find more date formats. There is not rocket science here and it is straight format.

Convert a formatted string to Date

Let’s now discussed about the reverse process to convert a string to Date object. Microsoft implemented a static method for Date object called “parseInvariant”. It returns a date if string is a valid representation of it, otherwise returns null.

// @value = A string that represents a date.
// @format = A string provided the format information. For me an array of strings did not work, even  Microsoft says this is the correct parameter type
 var a = Date.parseInvariant(value, format);

You can see below some examples.

var dt;

// This is valid date and is 1st of december 2015. 
dt = "2015-12-01";
Date.parseInvariant(dt,"yyyy-MM-dd");
dt = "12/01/2015";
Date.parseInvariant(dt,"dd/MM/yyyy");
dt = "01 December 2015";
Date.parseInvariant(dt,"dd MMMM yyyy");

It seems for “parseInvariant” function we use the same placeholders like we used for “format” function.

The functions I have talked about do not depend on the culture, but there are also other two versions which are affected by it: “parseLocale” and “localeFormat”. Documentation about entire Date type extended methods can be found here. I have to admit Microsoft did not provide too much details about this, but I hope my findings will help you work smarter doing client side development on web forms based technology (not only, because Ajax is not depending on server side).

Autocomplete textbox with JavaScript CSOM

I have searched on the internet about how to create an auto-complete functionality in SharePoint. Of course, jQuery UI was the solution with an Ajax request to REST service. For some reason, I cannot understand it, the examples I have seen are based on synchronous Ajax request. So I simply said no way. I needed something asynchronous to avoid page freeze.

Normally to create an auto-complete is a simple thing.


    jQuery('#txtBox').autocomplete({
        minLength: 3,
        source: function(request, response) {
         // At the and of the async operation call response with obtained results
         }
     });

Asynchronous operation will be placed inside source function, but instead using classic Ajax examples, I will use JavaScript CSOM. It is not better, but I like it more. So is more a personal choice.

For getting data from SharePoint, you can use classical example from MSDN website, but I prefer to reorganize the code a little bit. After all I still have Microsoft Ajax library available so I can put classes in namespaces or I can validate parameters type.

/// <Reference Name="MicrosoftAjax.js" />


Type.registerNamespace('Shp');


Shp.Lists = function () {
    throw 'Cannot instantiate Shp.Lists static class';
}


Shp.Lists.GetItems = function (listName, query, web, success, fail) {
    /// <summary>Get list items based on provided CAML query</summary>
    /// <param name="listName" type="String" optional="false" mayBeNull="false">List name</param>
    /// <param name="query" type="String" optional="false" mayBeNull="false">Query</param>
    /// <param name="web" type="SP.Web" optional="false" mayBeNull="true">Web</param>
    /// <param name="success" type="Function" optional="false" mayBeNull="false">Success callback</param>
    /// <param name="fail" type="Function" optional="true" mayBeNull="false">Fail callback</param>

    var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, mayBeNull: false, optional: false },
                                                   { name: 'query', type: String, mayBeNull: false, optional: false },
                                                   { name: 'web', type: SP.Web, mayBeNull: true, optional: false },
                                                   { name: 'success', type: Function, mayBeNull: false, optional: false },
                                                   { name: 'fail', type: Function, mayBeNull: false, optional: true }], true);
    if (e) throw e;

    var fail = fail || function (error) { alert(error); };
    var ctx = (web === null) ? SP.ClientContext.get_current() : web.get_context();
    var web = (web === null) ? ctx.get_web() : web;


    Shp.Lists._GetItems(listName, query, ctx, web, success, fail);
}

Shp.Lists._GetItems = function (listName, query, ctx, web, success, fail) {

    var oList = web.get_lists().getByTitle(listName);
    var camlQuery = new SP.CamlQuery();
    camlQuery.set_viewXml(query);
    var oListItems = oList.getItems(camlQuery);
    ctx.load(oListItems);
    ctx.executeQueryAsync(function () {
        success(oListItems);
    }, function (sender, args) {
        fail(args.get_message());
    });
}

Shp.Lists.registerClass('Shp.Lists');

“Shp.Lists.GetItems” should be called with the following parameters:

  • List name, as string, not optional and cannot be null.
  • CAML query as string, not optional and cannot be null.
  • Web as SP.Web, not optional but can be null. In this case web associated with the current context is used.
  • Success as function, not optional and cannot be null. It is executed if operation is a success.
  • Fail function, optional and cannot be null. If not specified and operation fails, code will alert the error message.

Now as I created a reusable function for reading list items, everything should be much easier. I just need to call “Shp.Lists.GetItems” with correct parameters inside auto-complete source and, if operation is successfully, to add suggestions based on list items.

jQuery('#txtBox').autocomplete({
    minLength: 3,
    source: function (request, response) {
       
        var term = request.term;
        var query = '<View><Query><Where><Contains><FieldRef Name="Title" /><Value Type="Text">' + term + '</Value></Contains></Where></Query></View>';
        Shp.Lists.GetItems("list name", query, null, function (items) {
            var suggestions = [];
            var listItemEnumerator = items.getEnumerator();
            while (listItemEnumerator.moveNext()) {
                suggestions.push(listItemEnumerator.get_current().get_item('Title'));
            }
            // Add suggestions
            response(suggestions);

        });

    }
});

This was my approach of creating the auto-complete functionality. As I said, is a personal option to use JavaScript CSOM because looks for me more organized and structured. Of course code can be extended and you can even create an Ajax client side control to incorporate this functionality.

Thank you for reading my post!

How did I add Angular in SharePoint apps

SharePoint 2013 development model is based on what is called SharePoint Apps. Typically, these are applications which rely on client side, including also Angular, more and more popular among developers. For me it was a little bit of a problem to get used with new development model, because I was used with client side development on classic web forms technology. So I have tried to combine what I knew with new way of doing things.

Because I did not want to interfere with existing master pages, I have created mine and simplified it to fit the functionality I want to achieve. So, I have added a module called “MasterPages” and change the name and extension of the sample text file, included by default in the module”, to “Layout.master”. Inside the master page I have placed the following content.

<!DOCTYPE html >
<html runat="server" dir="ltr" ng-app="holidayapp">
    <head runat="server">
         <meta http-equiv="X-UA-Compatible" content="IE=10" />
        <meta name="GENERATOR" content="Microsoft SharePoint" />
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <meta http-equiv="Expires" content="0" />
        <title><asp:ContentPlaceHolder runat="server" ID="PlaceHolderPageTitleInTitleArea" /></title>  
        <SharePoint:ScriptLink language="javascript" name="core.js" OnDemand="true" runat="server" Localizable="false"  />
        <SharePoint:ScriptLink language="javascript" name="sp.js" LoadAfterUI ="true" OnDemand="false" runat="server" Localizable="false" />
        <asp:ContentPlaceHolder runat="server" ID="laceHolderAdditionalPageHead" />      
    </head>
    <body>
        <form runat="server" id="aspnetForm">
         <asp:ScriptManager runat="server" AjaxFrameworkMode="Enabled" EnablePartialRendering="true" LoadScriptsBeforeUI="false" ScriptMode="Debug">
             <Scripts>
            </Scripts>
         </asp:ScriptManager>
          <asp:ContentPlaceHolder runat="server" ID="PlaceHolderMain">
             

        </asp:ContentPlaceHolder>
        </form>
    </body>
</html>

In the elements file, I have change the URL where master page should be deployed:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="MasterPages">
    <File Path="MasterPages\Layout.master" Url="_catalogs/masterpage/Layout.master" ReplaceContent="TRUE"  Type="GhostableInLibrary"/>
  </Module>
</Elements>

Beside classic master page code, you can notice I have added mark-up for Angular application. It remained to load required JavaScript files, for which I used script manager control. I see no reason why I should not use it. Loading script after user interface, it offers the best way of loading scripts in a web page as it does not affect performance.

<asp:ScriptManager runat="server" AjaxFrameworkMode="Enabled" EnablePartialRendering="true" LoadScriptsBeforeUI="false" ScriptMode="Debug">
 <Scripts>
<asp:ScriptReference Path="../../Scripts/angular.min.js" />
 </Scripts>
</asp:ScriptManager>

We do not have yet controller, but my intention is to add it to the content page. So, let’s move forward and change content page accordingly.

<%@ Page Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" MasterPageFile="~site/_catalogs/masterpage/Layout.master" Language="C#" %>
<asp:Content runat="server" ContentPlaceHolderID="PlaceHolderMain">
    <asp:ScriptManagerProxy runat="server"> 
        <Scripts>
            <asp:ScriptReference Path="../Scripts/Default.js"/>
        </Scripts>
    </asp:ScriptManagerProxy>
    
    <div ng-controller="maincontent">
        {{mymessage}}
    </div>
  
</asp:Content>

Please note the followings:

  • Content page is pointing now to my master page I have created before (see page directives).
  • Controller logic will be place in “Default.js” file, loaded through script manager proxy control. Again we use standard web forms technique of loading files.
  • Controller mark-up is placed in a content area.

As is not the scope of this article to discuss about Angular in details, logic of the controller is very simple.

var holidayApp = angular.module("holidayapp", []);

var content = holidayApp.controller("maincontent", function ($scope) {
    $scope.mymessage = "Message";
});

The idea behind this article is to show you can still use web forms and add modern technology to your project. Personally, I do not agree loading JavaScript files in the header. Script manager is still very powerful. And even more, you can combine this way Angular with Microsoft Ajax client side library.

Show versioning in a custom page

Customizing a SharePoint site (customizing means develop from scratch some pages with SharePoint Designer, of course based on a custom master page, and show and submit data from these pages), I was required to show version history for items in a custom list. Without server side, I had not fast way to do this. However, even if is not a nice solution, you can always read and parse “Versions.aspx” built-in application page. Let’s assume you already know list item and list id.

var listId = '{52CD7375-1125-4166-9741-DF8BFC0A2648}';
var listItemId = '140';

Create an empty table tag in your custom page and assign and ID.

<table id="versions"></table>

With some help from jQuery, is now the time to make an Ajax request to Versions.aspx, parse the response and populate our table with data.

jQuery.ajax({
    url: 'siteurl/_layouts/15/Versions.aspx?list=' + listId + '&Id=' + listItemId,
    cache: false,
    data: 'html',
    success: function (data) {
        var html = jQuery(data).find('.ms-settingsframe').html();
        jQuery('#versions').html(html);
    }
});

This code is doing the job. But there is only one thing left. You might be require to remove as much as possible direct access to the back-end, so a normal step is to remove “href” attributes for links and “onclick” events. In this case, you need to re-rewrite a little bit the Ajax request:

jQuery.ajax({
    url: 'siteurl/_layouts/15/Versions.aspx?list=' + listId + '&Id=' + listItemId,
    cache: false,
    data: 'html',
    success: function (data) {
        var html = jQuery(data).find('.ms-settingsframe').html();
        var noHref = /(href="([^>]+)")/ig;
        var noClicks = /(onclick="([^>]+)")/ig;
        html = html.replace(noHref, '').replace(noClicks, '');
        jQuery('#versions').html(html);
    }
});

As I told you, this is far away from being an elegant solution. But in some case it might be sufficient and you can start continue working on other tasks.