Image gallery from attachments in tooltips

Do you want to obtain a small image gallery display in a tooltip based on a files attached to an item?

Not too complicated to obtain this. We just need JavaScript tooltipster and DataTable library and CSS files. But first let’s build the code for getting attachments based on item id.

Type.registerNamespace('Shp');

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

 Shp.Lists.GetAttachments = function (listName, itemId, webUrl, success, fail) {
            /// <summary>Get attachments for an item</summary>
            /// <param name="listName" type="String" optional="false" mayBeNull="false">List name</param>
            /// <param name="itemId" type="Number" integer="true" optional="false" mayBeNull="false">List item</param>
            /// <param name="success" type="Function" optional="false" mayBeNull="false">Success</param>
            /// <param name="fail" type="Function" optional="true" mayBeNull="false">Fail</param>
            var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, mayBeNull: false, optional: false },
                                                            { name: 'itemId', type: Number, integer: true, mayBeNull: false, optional: false },
                                                            { name: 'webUrl', type: String, mayBeNull: true, optional: false },
                                                            { name: 'success', type: Function, mayBeNull: false, optional: false },
                                                            { name: 'fail', type: Function, mayBeNull: false, optional: true }], true);
            if (e) throw 'Shp.Lists.GetAttachments was called with invalid parameters. ' + e;
            var fail = fail || (function (err) { alert(err); });
            var ctx = (webUrl === null) ? SP.ClientContext.get_current() : new SP.ClientContext(webUrl);

            Shp.Lists._GetAttachments(listName, itemId, ctx, success, fail);
        };

     Shp.Lists._GetAttachments = function (listName, itemId, ctx, success, fail) {
            var oList = ctx.get_web().get_lists().getByTitle(listName);
            var oListItem = oList.getItemById(itemId);
            var files = oListItem.get_attachmentFiles();
            ctx.load(files);

            ctx.executeQueryAsync(function () {
                var results = [];
                for (var i = 0; i < files.get_count() ; i++) {
                    var file = files.itemAt(i);
                    results.push({ 'fileName': file.get_fileName(), 'serverRelativeUrl': file.get_serverRelativeUrl() });
                }
                success(results);
            }, function (sender, args) {
                fail(args.get_message());
            });
        };

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

Now, let have a look in XSLT code. We need it to display the data.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" xmlns:SharePoint="Microsoft.SharePoint.WebControls">
  
    <xsl:output method="html" indent="yes"/>
    <xsl:template match="/">


      <div class="card-body">
          <table class="table table-striped table-bordered nowrap" id="dataTable" width="100%" cellspacing="0">
            <thead>
              <tr>
                <th>ID</th>
                <th>Product<br />Name</th>
                <th>Product<br />Code</th>
                <th>Description</th>
                <th>Country</th>
                <th>Supplier</th>
                <th>Available<br />Qty</th>
                <th>Currency</th>
                <th>Price</th>
              </tr>
            </thead>
            <tbody>
              <xsl:for-each select="/dsQueryResponse/Rows/Row">
                <tr>
                  <td>
                    <a title="Loading..." class="product-images" data-id="{@ID}">
                      <xsl:value-of select="@ID" disable-output-escaping="yes" />
                    </a>
                  </td>
                  <td>
                    <xsl:value-of select="@ProductName" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@ProductCode" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@ProductDescription" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@CountryCode" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@Supplier" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@Qty" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@Currency" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@Price" disable-output-escaping="yes" />
                  </td>
                </tr>
              </xsl:for-each>
            </tbody>
          </table>

      </div>
         
    </xsl:template>
</xsl:stylesheet>

Our scope is to display a small image gallery in a toolip when we click in a link. So, for this, we need to run the following code when page loads.

jQuery('#dataTable').DataTable({
        scrollX: true,
        paging: false,
        fixedColumns: {
            leftColumns: 1
        },
        'aoColumnDefs': [
            { "bSortable": false, "aTargets": [0] }
        ],

        'drawCallback': function (settings) {
            jQuery('a[class*="product-images"]').tooltipster({
                trigger: 'click',
                contentAsHTML: true,
                interactive: true,
                theme: 'tooltipster-shadow',
                functionBefore: function (instance, helper) {
                    var $origin = jQuery(helper.origin);
                    var poId = $origin.attr('data-id');
                    if (Boolean($origin.data('loaded')) !== true) {
                        Shp.Lists.GetAttachments('Products list name', parseInt(poId), null, function (attachments) {
                            if (attachments.length === 0) {
                                instance.content('No image found');
                                $origin.data('loaded', true);
                                return;
                            }

                            var html = '';
                            for (var k = 0; k < attachments.length; k++) {
                                html += '<div class="mySlides">' +
                                    '<img src="' + attachments[k].serverRelativeUrl + '" style="width:100px; height: 100px">' +
                                    '</div>';
                            }
                            instance.content('<div class="slideshow-container">' + html + '</div>');

                        },
                            function (err) {
                                instance.content('Cannot get images:' + err);
                                $origin.data('loaded', true);
                            });
                    }
                }
            });
        }
    });

Of course we can apply different style on the gallery, but this is not the scope here. This is just to give you a start from where to start.

Advertisements

Token endpoint URL for SharePoint Online

To connect to SharePoint Online from external application, including Salesforce, you will need the token endpoint URL. This URL has this format:

https://accounts.accesscontrol.windows.net/[tenant]/tokens/OAuth/2?resource=[sender ID]/[sharepoint online host name]@[tenant]

Looks simple. You just need to replace tenant, sender ID and sharepoint online host name with real values:

  • Sender ID: 00000003-0000-0ff1-ce00-000000000000
  • Sharepoint online host name: can be found by URL where your site collection is located. For example, if you site collection URL is https://companyname.sharepoint.com/sites/sprint, host name will be companyname.sharepoint.com.
  • Tenant: is almost the same as host, just “sharepoint” is replaced with “onmicrosoft”. So if my host is companyname.sharepoint.com, tenant will be companyname.onmicrosoft.com.

Just replace the values and you will have a valid token endpoint URL to use for connecting your application to SharePoint Online. I will come back in another article with more details about how this is used in authentication and authorization process against SharePoint Online.

Ajax Toolkit date range pickers in SharePoint Online

I was always a fan of Microsoft Ajax Toolkit JavaScript. In the past was some documentation about how to use it only on client side, but now it seems the documentation is only about how to use it with server side components which generate for you JavaScript code. Unfortunately, you cannot do this on SharePoint Online, so I have decided to extract files from the latest version created under the governance of DevExpress and apply them in SharePoint Online environment.

Files preparation

Download files from this location, extract them from archive and upload them into a folder located on SharePoint. All of them should be in the same folder, so please don’t change their location relative to start.js file. Start.js file is an old files on demand loader created by Microsoft, which is not popular but I still consider to be the best option for what Microsoft has created.

Then, in your aspx file, using script manager or script proxy manager, add reference to these files.

<asp:ScriptManager runat="server" LoadScriptsBeforeUI="false">
<Scripts>
<asp:ScriptReference Path="../Common/Resources/Resources.js" />
<asp:ScriptReference Path="../Common/Start.js" />
<asp:ScriptReference Path="../Common/Extended.js" />
</Scripts>
</asp:ScriptManager>

Also, please create a reference to CSS file included in the archive.

Initialization

Now as we have already a loader, we don’t need to worry about loading order of the files or if the files are already loaded or not. We just need to place our logic into Sys.require function calling.

Sys.require([Sys.scripts.Calendar], function () {
// Logic is placed here
});

In the HTML mark-up, please create two SharePoint text input controls.

<SharePoint:InputFormTextBox runat="server" ClientIDMode="Static" ID="startDate" />
<SharePoint:InputFormTextBox runat="server" ClientIDMode="Static" ID="endDate" />

We should start now creating the logic. Starting from somewhere, we can create two simple independent date pickers.

    $create(Sys.Extended.UI.CalendarBehavior, { id: 'startDate', format: 'yyyy-MM-dd'}, {}, null, $get('startDate'));
    $create(Sys.Extended.UI.CalendarBehavior, { id: 'endDate', format: 'yyyy-MM-dd' }, {}, null, $get('endDate'));

If everything goes as expected, we should have now two date pickers with no relationship between them. However, selecting a range means end date should be greater or equal with start date, so we need to create a relationship. For this, we need to attach a handler for date change event on start date client control.

        $create(Sys.Extended.UI.CalendarBehavior, { id: 'startDate', format: 'yyyy-MM-dd' }, {
            dateSelectionChanged: function (instance) {

                var dt = instance.get_selectedDate();
                $find('endDate').set_startDate(dt);

                // Add this only if you want to adjust end date automatically according the start date selection
                $find('endDate').set_selectedDate(dt);

            }


        }, null, $get('startDate'));
        $create(Sys.Extended.UI.CalendarBehavior, { id: 'endDate', format: 'yyyy-MM-dd', startDate: $get('startDate').value }, {}, null, $get('endDate'));

I would say this is all you need for this functionality. What I like on this implementation is actually you can choose if your end date is automatically adjusted or not. When I have used jQuery date pickers, I couldn’t remove this option or it is hidden somewhere and I am not aware about. However, for complicated forms, customers requested to remove automatically adjustment, because users in general will use it to bypass validation and will start enter wrong information.

“Folder name is not valid” error trying to open a SharePoint Online site with SharePoint Designer

I was experiencing an error trying to open a SharePoint Online website with SharePoint Designer. I was receiving “Folder name is not valid” error and opened a request to SharePoint Online Support Team. I need to tell you the support was very good in this case. They figure out fast was an access denied issue causing the error.

It turned out the solution was an easy one, even I am not sure how happy customers can be with it. When you log to SharePoint Online using your credentials you need to check “Keep me signed in” option.

2015-03-20 11_37_34-Sign in to Office 365

After login step, you need to browse at least once website you are trying to open. This being told, you can try to apply this for future cases, until Microsoft will publish the solution on their site.

Create a custom web part page layout

As SharePoint customization is pretty much about creating custom page layouts, I think is a good point to discuss about how to create a custom web part page. For this a SharePoint Designer is the only tool you need, except the situation you need to package the solution to deliver to customers.

Create custom master page

Open the site in SharePoint Designer, navigate to _catalogs/masterpage folder and go to File – Blank Master Page menu item option. You will be prompted to specify the name of your master page, in my case being named “sitecustom.master”. Once you save the file, open “minimal.master” and copy all the code into new created master. This will assure a minimum functionality for page layout which derived from it. As an easier alternative, you can read this MSDN article about how to create from interface a minimal master page.

Create page layout

In the same folder, go to File – ASPX menu item option. This way, you will create a simple ASPX web form page, not linked yet to the master. You can delete entire code and add the following declarations (page directives).

<%@ Page Language="C#" MasterPageFile="~site/_catalogs/masterpage/sitecustom.master" inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" meta:progid="SharePoint.WebPartPage.Document" meta:webpartpageexpansion="full" %>
<%@ Register tagprefix="SharePoint" namespace="Microsoft.SharePoint.WebControls" assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register tagprefix="WebPartPages" namespace="Microsoft.SharePoint.WebPartPages" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

Version number can be different as is dependent by SharePoint version you are using. Mine is SharePoint Online and represents the latest version available.
You have successfully linked content page to master. However, an easier way is to select from File ribbon menu “New from master page” option. This should do the same thing, but if you choose this way, please be sure you have all required page directives.
Now your page is almost ready, but is still not editable because no web part zone is placed inside. Let’s do this and place web part zone inside “PlaceHolderMain” content placeholder.

<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
<WebPartPages:WebPartZone runat="server" Title="Zone 2" ID="CentralZone"><ZoneTemplate></ZoneTemplate></WebPartPages:WebPartZone>
</asp:Content>

You can create other content placeholders as well and populate them with web part zone. Is up to you here and you have freedom to customize everything. Just a small note from my side. If you are working in SharePoint Online or 2013, do not remove the tag below. It seems somehow is affecting edit panel for web part, making them not to be visible when you want edit something. Not sure why and what is happening there, but you better trust me.

<SharePoint:AjaxDelta id="DeltaPlaceHolderMain" IsMainContent="true" runat="server">
</SharePoint:AjaxDelta>

Associate page with content type

The last phase is to associate the page to a content type to allow creation of the pages using built-in functionality. To do this, you need to edit the properties of the page. You can be automatically directed to edit page from SharePoint Designer by clicking on the page in explorer view and choosing “Manage all file properties in the browser” option.
This being done, you need to make this page a page layout and give a value for associated content type. Below is my example.
WPartPage1
WPartPage2
Once you have done this, you need to test if functionality is correct. Just navigate to Page library, go to New Document menu option and select Web Page. On the next screen you should see the page you have just created available in the options list. Choose it and check if everything is working.
WPartPage3

Here is a very basic example about how to create a custom layout for web part pages, but I will cover complex scenarios in the next posts. But until next posts, I wish you “Happy Coding!”. 🙂

Read SharePoint Online list from ASP.NET

Today, IT world is all about connecting systems, so to connect an ASP.NET Web Forms based website to SharePoint Online can be a usual task for developers. This approach is classic and you can say is old fashion. We can say Web Forms are deprecate, but this is not true and this will be another topic on my blog.

But, let’s start with the beginning and present the steps you need for this.

Add required libraries to site

To be able to connect to SharePoint Online, or on Premise as well, you need to import some libraries into your website, or project if you decide to go for an ASP.NET project. These can be found on Microsoft site under the name of “SharePoint Server 2013 Client Components SDK” and can be downloaded from this address. Just copy and page these libraries to Bin folder you will enable SharePoint client objects into your code.

Create class to perform data operations

As you have imported required libraries, you can perform operations against a SharePoint list anywhere in your code. However, as you probably want a little bit of code organization, I would suggest you to create a class dedicated for this. I have called it “SharePoint”, which is probably not the best name, as it can create confusion.

using System;
using System.Collections.Generic;
using System.Security;
using System.Linq;
using System.Web;
using Microsoft.SharePoint.Client;

/// <summary>
/// Summary description for SharePoint
/// </summary>
public class SharePoint : IDisposable
{
    string username;
    SecureString password;
    string url;
    ClientContext context;

	public SharePoint(string url, string username, string password)
	{
        this.url = url;
        this.password = password.ToSecureString();
        this.username = username;
        this.context = new ClientContext(this.url);
        this.context.Credentials = new SharePointOnlineCredentials(this.username, this.password);
	}

  

   /// <summary>
   /// Get list items (method overloaded)
   /// </summary>
   /// <param name="listName">List name</param>
   /// <returns></returns>
    public Microsoft.SharePoint.Client.ListItemCollection GetListItems(string listName)
    {
        return this.GetListItems(listName, CamlQuery.CreateAllItemsQuery());
    }

    /// <summary>
    /// Get list items (method overloaded)
    /// <param name="listName">List name</param>
    /// <param name="camlQuery">CAML query</param>
    /// <returns></returns>
    public Microsoft.SharePoint.Client.ListItemCollection GetListItems(string listName, string camlQuery)
    {
        var oQuery = new CamlQuery();
        oQuery.ViewXml = camlQuery;
        return this.GetListItems(listName, oQuery);
    }

    /// <summary>
    /// Get list items (method overloaded)
    /// </summary>
    /// <param name="listName">List name</param>
    /// <param name="camlQuery">CAML query</param>
    /// <returns></returns>
    public Microsoft.SharePoint.Client.ListItemCollection GetListItems(string listName, CamlQuery camlQuery)
    {
        Web oWeb = context.Site.RootWeb;
        List oList = oWeb.Lists.GetByTitle(listName);
        Microsoft.SharePoint.Client.ListItemCollection oItems = oList.GetItems(camlQuery);
        context.Load(oItems);
        context.ExecuteQuery();
        return oItems;
    }
    
    void IDisposable.Dispose()
    {
        this.context.Dispose();
    }


    public static string GetFieldValue(object field = null)
    {
        if (field == null)
            return "";
        
        var fieldType = field.GetType().ToString();
       
    
        // user field
        if (fieldType  == "Microsoft.SharePoint.Client.FieldUserValue")
        {
            return ((Microsoft.SharePoint.Client.FieldUserValue)field).LookupValue;
        }
        // int32
        else if (fieldType == "System.Int32")
        {
            return Convert.ToString(field);
        }
        // string
        else if (fieldType == "System.String")
        {
            return Convert.ToString(field);
        }
        // Lookup field value
        else if (fieldType == "Microsoft.SharePoint.Client.FieldLookupValue") {
            return Convert.ToString(((Microsoft.SharePoint.Client.FieldLookupValue)field).LookupValue);
        }
        // date time
        else if(fieldType == "System.DateTime")
        {
            return Convert.ToDateTime(field).ToLongDateString();
        }
        else if (fieldType == "Microsoft.SharePoint.Client.ContentTypeId") {
            return ((Microsoft.SharePoint.Client.ContentTypeId)field).StringValue;
        }
        else
        {
            // return Convert.ToString(field);
            return fieldType;
        }   
    }
}

The class contains an overloaded method called “GetListItems” and a static method used for converting field values into string based on their types. There is a lot you can improve here, but it doesn’t fall under this post subject. Also we are not going to use for this example all signatures of “GetListItems” method, but for future use you can keep this method overloading.

Create Ajax enabled WCF service to communicate with SharePoint

What you need now is WCF service to communicate with SharePoint. I choose an Ajax enabled one because I wanted to use it through script manager. But if you prefer, you can use a non-Ajax enabled one to consume it using jQuery.

When you actually execute a query against a SharePoint list, you obtain a ListItemCollection object. As far as I know it cannot be de-serialized by default, so you will need to implement this. So you can create following classes to serve as data contract for the web service.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ServiceModel;
using System.Runtime.Serialization;


/// <summary>
/// Represent list item in a collection returned by WCF service
/// </summary>
[CollectionDataContract(Name = "SharePointListItem")]
public class SharePointListItem : Dictionary<string, string>
{
  
}


/// <summary>
/// Represents a collection of SharePoint list items returned by WCF service
/// </summary>
[CollectionDataContract(Name = "SharePointListItemCollection")]
public class SharePointListItemCollection : List<SharePointListItem>
{

}

Implementation of WCF service is simple. In our case, not to complicate the explanations too much, I have created only one method called “GetListItems”, which will get all items for specified list.

using System;
using System.Configuration;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;
using System.Xml;
using System.Web.Script.Serialization;
using Microsoft.SharePoint.Client;

[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class ListData
{
	// To use HTTP GET, add [WebGet] attribute. (Default ResponseFormat is WebMessageFormat.Json)
	// To create an operation that returns XML,
	//     add [WebGet(ResponseFormat=WebMessageFormat.Xml)],
	//     and include the following line in the operation body:
	//         WebOperationContext.Current.OutgoingResponse.ContentType = "text/xml";



    [WebGet(ResponseFormat=WebMessageFormat.Json)]
    public SharePointListItemCollection GetListItems(string listName)
    {
        string url = ConfigurationManager.AppSettings["spUrl"];
        string username = ConfigurationManager.AppSettings["spUsername"];
        string password = ConfigurationManager.AppSettings["spPassword"];
        SharePointListItemCollection results = new SharePointListItemCollection();
        using(SharePoint sp = new SharePoint(url,username, password))
        {
        
            Microsoft.SharePoint.Client.ListItemCollection items = sp.GetListItems(listName);
            foreach (ListItem item in items)
            {
                SharePointListItem r = new SharePointListItem();
                foreach (KeyValuePair<string, object> field in item.FieldValues)
                {
                    // SharePointItemField f = new SharePointItemField();
                    // f.FieldName = field.Key;
                    // f.FieldValue = SharePoint.GetFieldValue(field.Value);
                    r.Add(field.Key, SharePoint.GetFieldValue(field.Value));
                }
                results.Add(r);
            };
          
           
        }
        return results;
    }


}

As you noticed, SharePoint class is initialized with 3 parameters, and we are taking these from application settings. To you need to add them in web.config.

  <appSettings>
    <add key="spUsername" value="username" />
    <add key="spPassword" value="password" />
    <add key="spUrl" value="https://url" />
  </appSettings>

Add reference to the WCF service

As the WCF service is Ajax enabled, you need to add a reference to it script manager tag. This will generate a JavaScript for you and will eliminate time waste for writing Ajax calls to the service.

    <asp:ScriptManager runat="server" EnablePartialRendering="true" LoadScriptsBeforeUI="false" EnablePageMethods="true">
        <Services>
            <asp:ServiceReference Path="~/_Services/ListData.svc" />
        </Services>
     </asp:ScriptManager>

Getting the data

And this is the last step, getting the data from SharePoint list. This will take place when page loads to prevent performance issues rendering the mark-up.

function pageLoad(source, args) {
    var service = new ListData();
    service.GetListItems('List name',
        // Success
        function (response) {
            // Do something with JSON response
        },
        // Fail
        function (response) {
            alert(args.get_message());
        });
}

The response is JSON type, so it is up to you what you are going to do with it. My suggestion is to use JavaScript template engine to display it on the page.

SharePoint HTML 5 controls

Is clear for me, with every technological step in SharePoint environment, Microsoft is getting closer to modern development and HTML 5. And I am talking here about SharePoint:InputFormTextBox control, which is a sort of improved version of classic asp:TextBox control. But let’s what is new in SharePoint Online (most probably in 2013 as well, but I did not test it yet).

Selecting date/date and time

SharePoint:InputFormTextBox can be easily displayed as an HTML 5 date picker control. Is important how do you set text mode property.

<SharePoint:InputFormTextBox runat="server" TextMode="DateTimeLocal"  />

HTML5-calendar
However, being rendered as an HTML 5, is dependent of browser capabilities. In case it is not supported, you will still see a normal text input element.

As you can see, you also have a mask for entering the time. However, there is a simplified version to select only date (in fact is a different control, but please allow my poetic expression :)).

<SharePoint:InputFormTextBox runat="server" TextMode="Date"  />

HTML5-date
In a similar way you can have a control for entering a time value.

<SharePoint:InputFormTextBox runat="server" TextMode="Time"  />

HTML5-time

Range selector control

Range selector control is the second one on my list. Setting text mode to “Range” will do the trick. However, you can also additional HTML 5 range selector control attributes, like min and max value.

<SharePoint:InputFormTextBox runat="server" TextMode="Range" min="0" max="10"  />

HTML5-range

Color picker control

Even is not likely to use this control in an enterprise system, is still there and is a good thing to know you have it available.

<SharePoint:InputFormTextBox runat="server" TextMode="Color"  />

HTML5-color-picker

Number control

Number control can be a real help in developing forms. So having the same syntax and just adjusting the text mode property, you can have one in your page.

<SharePoint:InputFormTextBox runat="server" TextMode="Number" min="1" max="6"  />

As you can see, there is no rocket science to have some server side controls rendering HTML 5 client side elements. However, there is something more there you can do. It seems all of these controls can do postback. For me, it seems SharePoint will become soon a reliable environment for web developers. And I am waiting for the day when all the browsers will have full support HTML 5.