Second Life of a Hungarian SharePoint Geek

May 9, 2013

Extending the classes generated by SPMetal to access the login name of the users

Filed under: LINQ, SP 2010 — Tags: , — Peter Holpar @ 13:25

Recently I was working on a console application that collects data from SharePoint list using LINQ for further analysis. Assume we have a list called Developers (no specific Content Type, only the default Item) with a field User (Person or Group, Allow multiple selections: No, People Only). Of course, we have in fact a lot more lists and columns, but they do not affect the lesson of this post.

I’ve generated the object model for LINQ using SPMetal, using a configuration XML for the Developers list like this fragment:

<List Name="Developers">
  <ContentType Name="Item" Class="Developer" />
</List>

Problem: We need the login name of the users in our report, however, using the default generated classed only the display name (User property) and the ID (UserId property) of the user are accessible.

Solution: Fortunately, we can extend the generated partial classes, for example, in our case we should add a new LoginName property to the Developer class. To achieve that, we have to implement the ICustomMapping interface.

Code Snippet
  1. public partial class Developer : ICustomMapping
  2. {
  3.     [CustomMapping(Columns = new String[] { "LoginName" })]
  4.     public void MapFrom(object listItem)
  5.     {
  6.         this.LoginName = null;
  7.  
  8.         SPListItem item = (SPListItem)listItem;
  9.         if (item["User"] != null)
  10.         {
  11.             using (SPWeb web = item.Web)
  12.             {
  13.                 SPFieldUserValue usr = new SPFieldUserValue(web, (string)item["User"]);
  14.                 this.LoginName = (usr.User != null) ? usr.User.LoginName : null;
  15.             }
  16.         }
  17.     }
  18.  
  19.     public void MapTo(object listItem)
  20.     {
  21.         throw new NotImplementedException();
  22.     }
  23.  
  24.     public void Resolve(RefreshMode mode, object originalListItem, object databaseObject)
  25.     {
  26.     }
  27.  
  28.     public string LoginName { get; internal set; }
  29. }

Note: In our case, we need only to read the account name, so (as you can see from the code above) I’ve implemented only the MapFrom method, but not the MapTo and Resolve methods. A solution that is able to write back the value might be a bit more complex, especially when one considers various validation and error handling scenarios.

April 24, 2013

Creating a mail distributor system using the incoming mail feature of SharePoint

Filed under: Incoming email, Reflection, SP 2010 — Tags: , , — Peter Holpar @ 21:56

Wouldn’t it be great to implement your own custom logic to distribute mails to targeted addresses (for example, based on sender or subject of the mail) using SharePoint lists and event receivers? In this post I show you the fundamental technical issues and their solution to achieve that goal. My “custom logic” is quite simple, I send the same mail back to the sender, however you can build more sophisticated logic using the same technique, but more on that later.

Before starting Visual Studio, I’ve created a simple custom list called MailDistributor on my SharePoint site.

In Visual Studio I chose the Empty SharePoint Project template, and added a new List Email Event event receiver item.

Having these artifacts, I altered the default Elements.xml, to register the event receiver to the list I created earlier:

Code Snippet
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.     <Receivers ListUrl="Lists/MailDistributor">
  4.         <Receiver>
  5.             <Name>IncomingMailHandlerEmailReceived</Name>
  6.             <Type>EmailReceived</Type>
  7.             <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
  8.             <Class>MailDistributor.IncomingMailHandler</Class>
  9.             <SequenceNumber>10000</SequenceNumber>
  10.         </Receiver>
  11.     </Receivers>
  12. </Elements>

Regarding the code, the first step was to create the extension method GetMailMessage for the SPEmailMessage type to convert it to a MailMessage object (System.Net.Mail namespace). Fortunately, both of these object types have the same stream format in the background. This stream is directly accessible from the SPEmailMessage, however, MailMessage  is not creatable from the stream (or ByteArray / String) format. To solve this issue, I utilized the RxMailMessage type (copyright by Peter Huber, Singapore), that is a derived class of MailMessage with Stream and File support.

Code Snippet
  1. public static MailMessage GetMailMessage(this SPEmailMessage spEmailMessage)
  2. {
  3.     MailMessage result = null;
  4.     if (spEmailMessage != null)
  5.     {
  6.         result = RxMailMessage.CreateFromStream(spEmailMessage.GetMessageStream());
  7.     }
  8.  
  9.     return result;
  10. }

Below is the structure of the solution, highlighted the classes borrowed from Peter Huber.

image

The next challenge was to set the addressee (To property) of the MailMessage instance. Since there is no way to change this read-only property using the public methods of the type (in practice, you should set it already in the constructor), I had to apply my experience with Reflection, and set the private field to of the private field message of the MailMessage instance. Although I set only the To property in the example using the SetTo extension method, you could (and should!) set the Cc and Bcc fields as well. For example, clear these values to avoid perpetual sending / receiving the same message in the case the one of the Cc / Bcc fields contain the incoming mail address of the SharePoint list. To do that, you should implement the SetCc and SetBcc methods and from these methods call the SetMailAddressCollection method with the parameters “cc” and “bcc” accordingly.

Code Snippet
  1. public static void SetTo(this MailMessage mailMessage, MailAddressCollection mailAddressCollection)
  2. {
  3.     if ((mailMessage != null) && (mailAddressCollection != null))
  4.     {
  5.         SetMailAddressCollection(mailMessage, mailAddressCollection, "to");
  6.     }
  7. }
  8.  
  9. private static void SetMailAddressCollection(MailMessage mailMessage, MailAddressCollection mailAddressCollection, string fieldName)
  10. {
  11.     if ((mailMessage != null) && (mailAddressCollection != null) && (fieldName != null))
  12.     {
  13.         Type typeMailMessage = typeof(MailMessage);
  14.  
  15.         FieldInfo fi = typeMailMessage.GetField("message", BindingFlags.NonPublic | BindingFlags.Instance);
  16.  
  17.         if (fi != null)
  18.         {
  19.             object message = fi.GetValue(mailMessage);
  20.  
  21.             if (message != null)
  22.             {
  23.                 Type typeMessage = message.GetType(); // it is internal class System.Net.Mail.Message
  24.  
  25.                 FieldInfo fi2 = typeMessage.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
  26.  
  27.                 if (fi2 != null)
  28.                 {
  29.                     fi2.SetValue(message, mailAddressCollection);
  30.                 }
  31.             }
  32.         }
  33.     }
  34. }

In the event receiver, we convert the SPEmailMessage into a MailMessage instance, set its To property to the e-mail address of the original poster (From property of the MailMessage) and send the mail using an SmtpClient object. The Host property of the SmtpClient instance can be set using the Address of the configured SMTP server of the OutboundMailServiceInstance in the current web application.

As you can see, most of the code below is just tracing to help us to follow the process using DebugView. You are free to remove these lines without affecting the functionality, of course.

Code Snippet
  1. using System;
  2. using System.Diagnostics;
  3. using System.Net.Mail;
  4. using Microsoft.SharePoint;
  5. using Microsoft.SharePoint.Utilities;
  6.  
  7. namespace MailDistributor
  8. {
  9.     public class IncomingMailHandler : SPEmailEventReceiver
  10.     {
  11.         public override void EmailReceived(SPList list, SPEmailMessage emailMessage, string receiverData)
  12.         {
  13.             try
  14.             {
  15.                 Trace.TraceInformation("IncomingMailHandler starting…");
  16.  
  17.                 foreach (SPEmailHeader header in emailMessage.Headers)
  18.                 {
  19.                     Trace.TraceInformation("EmailReceived emailMessage header {0}, {1}", header.Name, header.Value);
  20.                 }
  21.  
  22.                 SmtpClient smtpClient = new SmtpClient();
  23.  
  24.                 smtpClient.Host = list.ParentWeb.Site.WebApplication.OutboundMailServiceInstance.Server.Address;
  25.  
  26.                 Trace.TraceInformation("IncomingMailHandler: getting mail message from stream");
  27.  
  28.                 MailMessage mailMessage = emailMessage.GetMailMessage();
  29.  
  30.                 Trace.TraceInformation("IncomingMailHandler: setting mail message To field");
  31.  
  32.                 mailMessage.SetTo(new MailAddressCollection { mailMessage.From });
  33.  
  34.                 Trace.TraceInformation("IncomingMailHandler: sending mail");
  35.  
  36.                 smtpClient.Send(mailMessage);
  37.                 
  38.                 Trace.TraceInformation("IncomingMailHandler: finished");
  39.             }
  40.             catch (Exception ex)
  41.             {
  42.                 Trace.TraceInformation("IncomingMailHandler exception: {0}", ex.Message);
  43.             }
  44.  
  45.         }
  46.  
  47.     }
  48.  
  49. }

After deploying the solution and activating the feature, we should enable the incoming mail for the MailDistributor list, set the mail address alias, and send a test message to this address. If there is no error, within about a minute we should receive the same mail, including formatting and attachments to the mailbox of the sender.

Using the Category settings of the mail to route the message

The rule we implemented is really a trivial one and has not much sense, but one can implement more complicated and more useful routing rules as well. My plan is to build a routing “engine” based on the Category settings of the mail.

As part of the Options / Tracking properties in Outlook, we can set not only Blue or Green categories, but our own custom categories (like SharePoint and Silverlight below) as well. As long as these categories are transferred within the mail, we can process them in our event receiver, look up SharePoint user profiles having the same values set in the Ask me about property, and route the message exactly to those users, implementing thus a simple but efficient knowledge management solution.

It would be even better if the user could choose those category values from a SharePoint Managed Metadata keyword list (a candidate for an Office 2013 mail-app?).

image

However there are some issues with the Category property that you should be aware of.

Based on this information, Outlook removes category settings from outgoing mails due to privacy concerns. One can alter this settings via registry (HKEY_CURRENT_USER\Software\Policies\Microsoft\Office\xx.0\Outlook\Preferences\SendPersonalCategories, where xx is the version number of Outlook, like 14 for Outlook 2010).

Exchange 2010 also removes the categories from the outgoing messages by default, as I’ve learned here. This behavior can be changes using the following PowerShell command:

Set-TransportConfig -ClearCategories:$False

A workaround for these issues might be an Outlook add-in, or a simply VBA code like this one, that illustrates, how to copy the mail categories to a custom mail header called X-Categories when sending the mail, thus avoiding losing of the categories:

Private Sub Application_ItemSend(ByVal item As Object, Cancel As Boolean)
    Dim mi As MailItem
    Set mi = item
    If Not mi Is Nothing Then
       item.PropertyAccessor.SetProperty "http://schemas.microsoft.com/mapi/string/{00020386-0000-0000-C000-000000000046}/X-Categories", item.Categories
    End If
End Sub

Of course, we should filter the mails affected by this behavior based on the To e-mail address, limiting it to the mails sent to our MailDistributor list.

April 17, 2013

Replacing the standard textbox for numeric fields with a Slider using HTML5

Filed under: HTML5, JavaScript, jQuery, SP 2010, SP 2013 — Tags: , , , , — Peter Holpar @ 11:39

HTML5 supports a special input field type called Range. It would be nice to use this type of control when we should provide the user an edit form to enter a limited range of integer values.

If this type of control is required in multiple lists, a straightforward option would be to create a custom field type. However, if it’s about only a single list, or deploying the assembly and other artifacts for the custom field is not allowed (like in the case of O365), than there is a simpler way to choose. We have to hide the original Text field for the numeric field and inject the Range control using JavaScript / jQuery. To persist the values set through the slider, we have to write back the changed values to the original, hidden Text field.

I’ve created a new custom list called Values on my O365 site, with a numeric field Value. The jQuery library was uploaded to a document library called scripts. Then I injected the following script block using a Content Editor web part into the NewForm.aspx / EditForm.aspx pages:

Code Snippet
  1. <script language="javascript" src="/scripts/jquery-1.8.3.min.js" type="text/javascript"></script>
  2. <script language="javascript" type="text/javascript">
  3.  
  4.     $(document).ready(startAction);
  5.  
  6.     function startAction() {
  7.         // hide original Text input field
  8.         $("input[title$=Value]").hide();
  9.         // inject our range control that allows entering values between 0 and 10
  10.         $("input[title$=Value]").before('<input id="myRange" type="range" min="0" max="10"/>&nbsp;<span id="myRangeValue"/>');
  11.  
  12.         var cv = $("input[title$=Value]").val();
  13.  
  14.         // if the value is empty, set a default value of 0
  15.         cv = (cv == "") ? 0 : cv;
  16.         $("input[title$=Value]").val(cv);
  17.  
  18.         // synchronize initial values
  19.         $("input#myRange").val(cv);
  20.         $("span#myRangeValue").html(cv);
  21.  
  22.         // register change event handler
  23.         $("input#myRange").change(function() { updateValue(); });
  24.     }    
  25.  
  26.     function updateValue() {
  27.         var cv = $("input#myRange").val();
  28.         $("input[title$=Value]").val(cv);
  29.         $("span#myRangeValue").html(cv);
  30.     }
  31.  
  32. </script>

The figure below illustrates the effect of the customization. Of course, you need to have a HTML5-compatible browser, like IE10 and the browser compatibility mode correctly set using IE developer tools (F12):

image

And what’s with SharePoint 2010? There are options to enable HTML5 for SharePoint 2010 (see a solution here), however be aware, that it may have negative impacts on other vital SharePoint features, as described here. I’ve tested the same solution with SP2010, and it seems to be OK, nevertheless, I suggest you to turn on HTML5 support only for the affected pages, setting their master page to a custom one as described in the referenced article, and not altering the default master page for the site.

How to use LINQ expressions in your JavaScript code when accessing the REST API (and other useful JavaScript libraries)

Filed under: JavaScript, jQuery, LINQ, Project Server, REST, SP 2010, SP 2013 — Tags: , , , , , , — Peter Holpar @ 04:24

Working with both C# and JavaScript to process and display information stored on Project Server, I had to realize the power of LINQ in C#, and the lack of this language feature in JavaScript. Fortunately, after a quick search I found the LINQ for JavaScript library, that enables a very similar syntax. Below I show a few examples to give you a highlight of its features.

The other handy library I use these days frequently is Datejs, that enables effective handling of the Date objects in JavaScript.

Although the code samples in this post are related to Project Server, you can apply the same technique to SharePoint as well. For the sake of simplicity I post only code snippets, and not the full code of the application. Hopefully it will be enough to demonstrate the power of these libraries.

LINQ for JavaScript

Note, that I’m using version 3.0.3-Beta4 in the samples below, and not the stable 2.2.0.2 version. There are considerable syntax differences between these versions, like lower case vs. upper case function names, so be sure the check the reference.htm coming with the actual version of your choice.

See my former post about using and limitations of the $expand query option.

In the first step I send a few REST request to the server and store the results in JavaScript object trees.

The sample below sends a REST request to the Project Server to query information, like Name, Id and ResourceCalendarExceptions properties of the enterprise resources as well as the Name of their BaseCalendar property. We need the response as a JSON stream, so we set the Accept header to ‘application/json; odata=verbose‘. If the call was successful, the result is stored in the resources variable.

var siteFullUrl = ‘http://yourProjectSite&#8217;;

var resources;
var calendars;
var assignments;

$.ajax({
                type: ‘GET’, 
                contentType: ‘application/json;odata=verbose’,
                url: siteFullUrl + "/sites/pwa/_api/ProjectServer/EnterpriseResources?$select=Name,Id,ResourceCalendarExceptions,BaseCalendar/Name&$expand=ResourceCalendarExceptions,BaseCalendar/Name",
                headers: {
                    ‘X-RequestDigest’: $(‘#__REQUESTDIGEST’).val(),
                    ‘Accept’: ‘application/json; odata=verbose’
                },
                dataType: ‘json’,
                complete: function (result) {
                    var response = JSON.parse(result.responseText);
                    if (response.error) {
                        alert("Error: " + response.error.code + "\n" + response.error.message.value);
                    }
                   else {
                       resources = response.d.results;
                   }
});

We can request the enterprise calendar data (name and exceptions) using a similar call of jQuery AJAX method, but in this case the URL of the REST query looks like this one below:

"/sites/pwa/_api/ProjectServer/Calendars?$select=Name,BaseCalendarExceptions&$expand=BaseCalendarExceptions"

and we store the response in the calendars variable.

calendars = response.d.results;

To to get the assignments for March I submitted a REST request with the following URL:

"/sites/pwa/_api/ProjectData/Assignments?$select=ResourceId,ProjectName,AssignmentStartDate,AssignmentFinishDate,AssignmentBookingId&$filter=AssignmentStartDate+le+datetime’2013-03-31′ and AssignmentFinishDate+ge+datetime’2013-03-01′"

and stored the results in the assignments variable:

assignments = response.d.results;

Having all of the responses on the client side, we can process them using a code like this one:

// default enterprise base calendar
var wdExcs = calendars[0].BaseCalendarExceptions.results;

$.each(resources, function (index) {
        // get the calendar exceptions of the current resource
        var wdExcsPriv = this.ResourceCalendarExceptions.results;
        var resId = this.Id;

        // get all assignments of the current resource
        var assignmentsPriv = Enumerable.from(assignments).where(function(x) { return (x.ResourceId == resId); }).toArray();

        // get the first day of the current month, that was 1st of March at the time of test
        var day = Date.today();
        var day = day.moveToFirstDayOfMonth();
        var dayOfWeek = day.getDay();

        var assignmentsForThisDay = Enumerable.from(assignmentsPriv).where(function(x) { return (new Date(parseInt(x.AssignmentStartDate.substr(6))) <= day) && (new Date(parseInt(x.AssignmentFinishDate.substr(6))) >= day) }).toArray();

        // is it a weekend, a base calendar exception (like state holidays) or a resource exception (private holiday)?
        var isHoliday = ((dayOfWeek == 0) || (dayOfWeek == 6)
          || (Enumerable.from(wdExcs).any(function(x) { return (Date.parse(x.Start) <= day) && (Date.parse(x.Finish) >= day) }))
          || (Enumerable.from(wdExcsPriv).any(function(x) { return (Date.parse(x.Start) <= day) && (Date.parse(x.Finish) >= day) })));

        // is there at least a single committed assignment or only proposed ones?
        var isBooked = (Enumerable.from(assignmentsForThisDay).any(function(x) { return (x.AssignmentBookingId==0); }));

        // aggregate (join) the assignment names
        var projects = (Enumerable.from(assignmentsForThisDay).select("$.ProjectName")).join();

});

Datejs

You can see Datejs in action in the previous code snippet, like the call of the moveToFirstDayOfMonth function above.

It has a lot of other useful features, for example, to get the number of days in the current month:

var month = today.getMonth();
var year = today.getFullYear();
var dayCount = Date.getDaysInMonth(year, month);

The getDayDiff function (requires time.js) returns the number of days between two dates:

function getDayDiff(time1, time2) {
  var timeSpan = new TimeSpan(time1 – time2);
  var dayCount = timeSpan.getDays() ;
  return dayCount;
}

The next snippet sets the variable firstDay to the first day of the current month, and lastDay to the last day of the following month:

var firstDay = Date.today();
firstDay.moveToFirstDayOfMonth();

var lastDay = Date.today().add(1).month();
lastDay.moveToLastDayOfMonth();

I’ve been working with these libraries for a few weeks now, and I can say they made my coding more efficient, so I think they worth a try if you have to work with complex REST responses and Date objects from JavaScript, that is very likely if you would like to implement some exciting SharePoint applications based on the new app model.

March 28, 2013

Dynamically populating Group Calendars, Part I – the basics

Filed under: Calendar, JavaScript, jQuery, SP 2010 — Tags: , , , — Peter Holpar @ 13:54

A few month ago I was working on a prototype of an internal SharePoint application to make it easier to HR and project managers to track and administer the status (like holiday, sick leave) of the employees.

My plan included a group calendar to administer the employee status (note: this feature seems to be already obsolete in SP 2013, see section Group Work site template and Group Work solution on TechNet). User should be able to choose a project from the ribbon, and the calendars of the project members would be displayed in the group calendar view. The ribbon extension itself was not difficult (I will discuss it in the next part of this post), however the dynamic refresh of the group calendar UI was really a bit challenging. The main topic of this post is how to add / remove users to / from the group calendar.

The only post I found on a similar issue is related to resources (I recommend you to read that post first, as I don’t repeat all of the details discussed there!), but it provided at least a good starting point for me, and the discovery of the JavaScript libraries and a lot of fiddlering began…

I won’t describe here all the steps I followed, but rather the most important results.

Note: that the methods and properties below are not officially documented, and using them from your custom code is not supported. Be aware, that they can be changed due to Service Packs or other updates without any prior notice, immediately breaking solutions build upon these techniques, so use them at your own risks.

The bulk of the functionality related to the group calendar is encapsulated into the SP.UI.ApplicationPages.Calendar.js (see the debug version: SP.UI.ApplicationPages.Calendar.debug.js). If not mentioned otherwise, all of the described JS objects can be found in this file.

The SP.UI.ApplicationPages.ResolveEntity (defined in SP.js / SP.debug.js) is primarily a data container to hold information on a calendar entity resolved by SharePoint.

When you add a new user, group or resource entity to the group calendar (for example using the People Picker, either the text box or the dialog), or add / edit / delete events, the calendar control sends an asynchronous request to the server to resolve the entity to be able to refresh the UI, and the “Loading calendar…” message is displayed.

image

To provide the async communication between the calendar control on the webpage and the data on the server side, the CalendarService.ashx ASP.NET Web Handler is used. For example, if we are adding the calendar of the CONTOSO Administrator to the group calendar the following data is send through a POST request to /_layouts/CalendarService.ashx:

cmd=query&listName=ecd80beb-7ba7-4ad0-810a-3e2a95f07c30&viewName=e7d89ac0-7bd6-42b7-9ee5-de7e8b4773bb&dataSourceId=00000000-0000-0000-0000-000000000000&viewType=weekgroup&entity=1;#CONTOSO\administrator;Administrator@contoso.com&selectedDate=&options=1

image

The response is a JSON representation of the events for the Administrator (assuming this user has a single meeting as illustrated above):

[{"Options":1,"Table":null,"DatePicker":null,"Dates":null,"RangeJDay":null,"Navs":null,"Items":{"Data":[[0,1,2,150563,150563,3,3,4,5,5,0,60,0,0,0,6,7]],"Strings":["8","Admin event","Office","3/25/2013","5:00 am","6:00 am","","0x7fffffffffffffff"]}}]

or as visualized by Fiddler:

image

Parameters sent to the CalendarService.ashx should be straightforward. The parameter entity is the key of the entity (in this case 1;#CONTOSO\administrator;Administrator@contoso.com) to be resolved, we have this value from the People Picker. I don’t discuss now the process, how the People Picker gets this values when resolving users asynchronously, it would worth another post.

See the get_key function of the ResolveEntity object to understand how the key for different entity types are built up. For example, in case of a user having an e-mail address the key ($8_0 property of the ResolveEntity) looks like this:

this.$8_0 = ’1;#’ + this.accountName + ‘;’ + this.email;

Compare this pattern with the value we used above for CONTOSO\administrator.

The entityType property of the ResolveEntity defines the type of entity. Supported values are:

SP.UI.ApplicationPages.ResolveEntity.typE_EVENT = ’0′;
SP.UI.ApplicationPages.ResolveEntity.typE_USER = ’1′;
SP.UI.ApplicationPages.ResolveEntity.typE_RESOURCE = ’2′;
SP.UI.ApplicationPages.ResolveEntity.typE_EXCHANGE = ’3′;

The value typE_USER means either a user or a group, the isGroup property of the ResolveEntity object makes a difference between user and group subtypes.

How to add entities to the calendar?

We should use the SP.UI.ApplicationPages.CalendarSelector (defined in SP.js / SP.debug.js) and the SP.UI.ApplicationPages.RibbonCalendarSelector (defined in SP.UI.ApplicationPages.Calendar.js / SP.UI.ApplicationPages.Calendar.debug.js) objects to achieve this goal.

// calSel is of tpye SP.UI.ApplicationPages.CalendarSelector
var calSel = SP.UI.ApplicationPages.CalendarSelector.instance();
// sel is of tpye SP.UI.ApplicationPages.RibbonCalendarSelector
var sel = calSel.getSelector(1, scopeKey);
sel.selectEntities(entitiesXml, false);

By calling the getSelector function the the fix value of 1 means a user entity type, and scopeKey is the context identifier for the calendar, and can be extracted from the page content when working with jQuery:

var calRootDiv = jQuery(".ms-acal-rootdiv");
var scopeKey = jQuery(calRootDiv).attr(‘ctxid’);

The entities defined by the entitiesXml will be appended to the existing list of items in the calendar. The format of the XML is shown for resources in the original post from Thomas Zepeda McMillan, and will be further investigated for user entities in my forthcoming post.

How to remove entities from the calendar?

You can think that setting the Append attribute of the Entities node of the XML we pass as parameter to the selectEntities function to a value to False has the effect that the new entities would be not appended to the existing list of entities, but the former entities would be cleared first, however, based on my experience, that is not the case. So we have to find a way to remove existing entities first.

As a first step, we could lookup the SP.UI.ApplicationPages.CalendarContainer for the specific scope (the SP.UI.ApplicationPages.CalendarInstanceRepository object is defined in SP.js / SP.debug.js):

var calCont = SP.UI.ApplicationPages.CalendarInstanceRepository.lookupInstance(scopeKey);

Using this CalendarContainer we can get a reference to the SP.UI.ApplicationPages.EntityPaginator:

var entPag = calCont.$a_1;

Entities displayed on the calendar are stored as an  Array of SP.UI.ApplicationPages.ResolveEntity objects:

var arr = entPag.$1y_1;
// just test: display some of the properties
alert(arr[0].entityType + ‘, ‘ + arr[0].get_key() + ‘, ‘ + arr[0].displayName + ‘, ‘ + arr[0].accountName);

The $AH function of the EntityPaginator removes the item with the specified index from the array plus refreshes the UI. For example, to remove first entity we should call:

entPag.$AH(0);

To remove all of the items, we should iterate through the array:

while(arr.length > 0) {
  entPag.$AH(0);
}

The $5P_1 method of EntityPaginator notifies all event handlers of "pagechanged" to force a control refresh. We should be able to remove all of the entities via clearing the Array content and call the $5P_1 method to refresh the UI directly as:

Array.clear(arr); // or Array.clear(entPag.$AH);
entPag.$5P_1();

however I found that this method – although might seem OK at first sight – is not working perfectly, especially when one would like to page in calendar between weeks.

Handling calendar paging / refresh events

Although not strictly related to the topic, it may be useful to know, that we can subscribe our custom event handlers for such events.

Using the add_$9x and the remove_$9x functions of the EntityPaginator can we inject (and remove) our event handlers for calendar refresh events (like paging):

entPag.add_$9x(Function.createDelegate(null, function() { alert(‘paged’); } ));

or alternatively we can apply a direct approach like this one:

function pageChanged() {
  alert(‘page changed’);
}

entPag.get_events().addHandler(‘pagechanged’, pageChanged);

Next steps

In this post I discussed the fundamentals we need to fulfill the original goal to create a dynamic group calendar view. In the next post I show you how to develop the ribbon extension that utilizes the methods described now.

March 5, 2013

Deleting files from the IE cache as part of the Visual Studio deployment process

Filed under: CKS.Dev, Internet Explorer, SP 2010, Visual Studio, VSX — Tags: , , , , — Peter Holpar @ 21:44

In the past months I had again a lot to do with client side SharePoint development, that means in practice mainly projects including ribbon extensions and tons of JavaScript files. One of the issues with that type of development is the bad habit of Internet Explorer called caching. For example, when you make modifications to the .js files, and re-deploy your project, it is rather common, that IE executes the former versions of the scripts, that I find pretty annoying.

Some of the workarounds I found for that issue in the past years:

Option Nr.1: Disable caching in IE (Internet options / General / Browsing history / Settings). It is OK if you use your environment only for development (like a Dev-VM), but not optimal if you should use the same browser to fulfill your other daily tasks.

image

Option Nr.2: Type the full URL of the script in the address text box of IE, download a local copy of the script to a temporary folder, and refresh the page with Ctrl+F5. Rather cumbersome method, especially if you have to refresh several scripts .

Option Nr.3: Open the location of the cache (Internet options / General / Browsing history / Settings / View files, marked with blue on the screenshot above), and delete the file(s) manually. The location in the file system is typically C:\Users\UserName\AppData\Local\Microsoft\Windows\Temporary Internet Files. Works quite good, but it takes some time and is still something you can forget.

Wouldn’t it be more comfortable to automate the process, for example, as a deployment step in Visual Studio? Definitely, but how to achieve that? Well, deleting files from the IE cache programmatically is far from being straightforward, but fortunately I found a nice example on MSDN for a sample wrapper class in C#, including a lot of PInvoke calls. Having this solution, the Visual Studio Extensibility part of the exercise was a routine task.

My first idea was to contribute this VS extension to the CKS.Dev project on CodePlex (see some posts on my other contributions to CKS.Dev here), but the project is just being upgraded to SharePoint 2013 / Visual Studio 2012, so I extended my own extension (documented here) instead.

Expected functionality:

I would like to specify through Visual Studio project properties, which files should be deleted from the cache (e.g. from the Temporary Internet Files folder).

First, I introduce a new property called IE cache delete rule that determines the scope of the action and has the following possible values:

Only from current site – only matching files cached from the active SharePoint site (the one you specified as the target of the deployment in VS) are deleted.

All from current sites – all files cached from the active SharePoint site are deleted, other filters are ignored.

No site specific – all cached files that fulfill the filters are deleted, independently from the origin URL.

Filters (there is a logical OR between the filters that means a cached file should fulfill at least one of the conditions to be deleted):

IE cache file list – It is a list of the names of the files to be deleted from the cache.

IE cache file pattern – It is a regular expression pattern. All files matching the pattern will be deleted from the cache, as long as it also matches the scope of the action (see IE cache delete rule property above)

Let’s have a look at the implementation!

My original SPVSProjectProps class was extended with the new properties:

Code Snippet
  1. // IECacheFilePattern property related members
  2. private const string IECacheFilePatternPropertyId = "IECacheFilePattern";
  3. private const string IECacheFilePatternPropertyDefaultValue = "";
  4.  
  5. [DisplayName("IE cache file pattern")]
  6. [DescriptionAttribute("This property specifies a regular expression pattern to determine which files should be deleted from the IE cache when the Clear IE cache deployment step is activated")]
  7. [DefaultValue(IECacheFilePatternPropertyDefaultValue)]
  8. // we want our custom property to show up in the SharePoint section in the project properties property grid
  9. [Category("SharePoint")]
  10. public string IECacheFilePattern
  11. {
  12.     get
  13.     {
  14.         string propertyValue;
  15.         int hr = projectStorage.GetPropertyValue(IECacheFilePatternPropertyId, string.Empty,
  16.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue);
  17.  
  18.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  19.         if (!ErrorHandler.Succeeded(hr) || String.IsNullOrEmpty(propertyValue))
  20.         {
  21.             propertyValue = IECacheFilePatternPropertyDefaultValue;
  22.         }
  23.  
  24.         return propertyValue;
  25.     }
  26.  
  27.     set
  28.     {
  29.         projectStorage.SetPropertyValue(IECacheFilePatternPropertyId, string.Empty,
  30.             (uint)_PersistStorageType.PST_PROJECT_FILE, value);
  31.     }
  32. }
  33.  
  34. // IECacheFileList property related members
  35. private const string IECacheFileListPropertyId = "IECacheFileList";
  36. private const string IECacheFileListPropertyDefaultValue = "";
  37.  
  38. [DisplayName("IE cache file list")]
  39. [Description("This property specifies which files should be deleted from the IE cache when the Clear IE cache deployment step is activated")]
  40. [DefaultValue(RelatedTimerJobsPropertyDefaultValue)]
  41. // we want our custom property to show up in the SharePoint section in the project properties property grid
  42. [Category("SharePoint")]
  43. // use custom property editor to avoid "Constructor on type 'System.String' not found." error in design mode
  44. [Editor("System.Windows.Forms.Design.StringArrayEditor, System.Design", typeof(UITypeEditor))]
  45. [TypeConverter(typeof(CsvArrayConverter))]
  46. public String[] IECacheFileList
  47. {
  48.     get
  49.     {
  50.         String propertyValue;
  51.         int hr = projectStorage.GetPropertyValue(IECacheFileListPropertyId, string.Empty,
  52.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue);
  53.  
  54.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  55.         if (!ErrorHandler.Succeeded(hr) || String.IsNullOrEmpty(propertyValue))
  56.         {
  57.             propertyValue = IECacheFileListPropertyDefaultValue;
  58.         }
  59.  
  60.         // remove accidental whitespaces
  61.         String[] fileNames = propertyValue.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
  62.         fileNames = Array.ConvertAll<String, String>(fileNames, fileName => fileName.Trim());
  63.         return fileNames;
  64.     }
  65.  
  66.     set
  67.     {
  68.         String propertyValue =
  69.             (value == null) ?
  70.             String.Empty :
  71.             // remove accidental whitespaces
  72.             String.Join("|", Array.ConvertAll<String, String>(value, fileName => fileName.Trim()));
  73.         projectStorage.SetPropertyValue(IECacheFileListPropertyId, string.Empty,
  74.             (uint)_PersistStorageType.PST_PROJECT_FILE, propertyValue);
  75.     }
  76. }
  77.  
  78. // IECacheDeleteRule property related members
  79. private const string IECacheDeleteRulePropertyId = "IECacheDeleteRule";
  80. private const IECacheDeleteRules IECacheDeleteRulePropertyDefaultValue = IECacheDeleteRules.OnlyCurrentSite;
  81.  
  82. [DisplayName("IE cache delete rule")]
  83. [Description("This property specifies the relation between the current SharePoint site and files to be deleted from IE cache")]
  84. [DefaultValue(IECacheDeleteRulePropertyDefaultValue)]
  85. // we want our custom property to show up in the SharePoint section in the project properties property grid
  86. [Category("SharePoint")]
  87. [TypeConverter(typeof(IECacheDeleteRuleConverter))]
  88. public IECacheDeleteRules IECacheDeleteRule
  89. {
  90.     get
  91.     {
  92.         // set default value
  93.         IECacheDeleteRules propertyValue = IECacheDeleteRulePropertyDefaultValue;
  94.         string propertyValueString;
  95.         int hr = projectStorage.GetPropertyValue(IECacheDeleteRulePropertyId, string.Empty,
  96.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValueString);
  97.  
  98.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  99.         if (ErrorHandler.Succeeded(hr) && !String.IsNullOrEmpty(propertyValueString))
  100.         {
  101.             Enum.TryParse<IECacheDeleteRules>(propertyValueString, out propertyValue);
  102.         }
  103.  
  104.         return propertyValue;
  105.     }
  106.  
  107.     set
  108.     {
  109.         projectStorage.SetPropertyValue(IECacheDeleteRulePropertyId, string.Empty,
  110.             (uint)_PersistStorageType.PST_PROJECT_FILE, value.ToString());
  111.     }
  112. }
  113.  
  114. public enum IECacheDeleteRules
  115. {
  116.     [Description("Only from current site")]
  117.     OnlyCurrentSite,
  118.     [Description("All from current site")]
  119.     AllFromCurrentSite,
  120.     [Description("No site specific")]
  121.     NoSiteSpecific
  122. }
  123.  
  124. // based on EnumConverter example from
  125. // http://www.c-sharpcorner.com/uploadfile/witnes/using-propertygrid-in-net/
  126. class IECacheDeleteRuleConverter : EnumConverter
  127. {
  128.     private Type enumType;
  129.  
  130.     public IECacheDeleteRuleConverter(Type type) : base(type)
  131.     {
  132.         enumType = type;
  133.     }
  134.  
  135.     public override bool CanConvertTo(ITypeDescriptorContext context, Type destType)
  136.     {
  137.         return destType == typeof(string);
  138.     }
  139.  
  140.     public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destType)
  141.     {
  142.         FieldInfo fi = enumType.GetField(Enum.GetName(enumType, value));
  143.         DescriptionAttribute dna = (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));
  144.         if (dna != null)
  145.             return dna.Description;
  146.         else
  147.             return value.ToString();
  148.     }
  149.  
  150.     public override bool CanConvertFrom(ITypeDescriptorContext context, Type srcType)
  151.     {
  152.         return srcType == typeof(string);
  153.     }
  154.  
  155.     public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
  156.     {
  157.         foreach (FieldInfo fi in enumType.GetFields())
  158.         {
  159.             DescriptionAttribute dna =
  160.             (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));
  161.             if ((dna != null) && ((string)value == dna.Description))
  162.                 return Enum.Parse(enumType, fi.Name);
  163.         }
  164.         return Enum.Parse(enumType, (string)value);
  165.     }
  166. }

We have two new methods to the static ExtensionHelper class. The ShouldDeleteIECacheFile method encapsulates the logic of cache file deletion as function of our new project properties. The ClearIECacheFile method calls the core cache file deletion (see ClearIEFiles method of the DeleteIECache class below) with this logic injected as a parameter. We do some logging in both of these methods to inform users in the Output window of VS about the progress and result of the deployment. We should keep the list of deleted files in the local filesDeleted variable, otherwise each files would be listed twice due to the logic implemented in the ClearIEFiles method.

Code Snippet
  1. internal static void ClearIECacheFile(ISharePointProject project)
  2. {
  3.     try
  4.     {
  5.         LogToOutputWindows(project, "Clearing files from IE cache");
  6.         SPVSProjectProps propertiesObject;
  7.         if (project.Annotations.TryGetValue<SPVSProjectProps>(out propertiesObject))
  8.         {
  9.             String ieCacheFilePattern = propertiesObject.IECacheFilePattern;
  10.             IEnumerable<String> ieCacheFileList = propertiesObject.IECacheFileList;
  11.             SPVSProjectProps.IECacheDeleteRules ieCacheDeleteRule = propertiesObject.IECacheDeleteRule;
  12.             List<String> filesDeleted = new List<String>();
  13.  
  14.             Func<Uri, bool> shouldDelete = new Func<Uri, bool>(u => ShouldDeleteIECacheFile(u,
  15.                                                                                         ieCacheFilePattern,
  16.                                                                                         ieCacheFileList,
  17.                                                                                         ieCacheDeleteRule,
  18.                                                                                         filesDeleted,
  19.                                                                                         project));
  20.             DeleteIECache.ClearIEFiles(shouldDelete);
  21.  
  22.             LogToOutputWindows(project, String.Format("Number of files deleted from IE cache: {0}", filesDeleted.Count));
  23.         }
  24.     }
  25.     catch (Exception ex)
  26.     {
  27.         LogToOutputWindows(project, String.Format("Clearing files from IE cache failed. Exception: {0}", ex.Message));
  28.     }
  29. }
  30.  
  31. internal static bool ShouldDeleteIECacheFile(Uri uri, String filePattern, IEnumerable<String> fileList,
  32.     SPVSProjectProps.IECacheDeleteRules ieCacheDeleteRule, List<String> filesDeleted, ISharePointProject project)
  33. {
  34.     bool result = false;
  35.  
  36.     Uri siteUrl = project.SiteUrl;
  37.     Regex filePatternRegex = string.IsNullOrEmpty(filePattern) ? null : new Regex(filePattern);
  38.     List<string> fileListEx = (fileList == null) ? new List<string>() : fileList.ToList();
  39.     string fileName = uri.Segments[uri.Segments.Length - 1];
  40.  
  41.     bool isFromCurrentSite = uri.AbsoluteUri.IndexOf(siteUrl.AbsoluteUri, StringComparison.InvariantCultureIgnoreCase) == 0;
  42.     if (ieCacheDeleteRule == SPVSProjectProps.IECacheDeleteRules.AllFromCurrentSite)
  43.     {
  44.         result = isFromCurrentSite;
  45.     }
  46.     else
  47.     {
  48.         result = ((fileListEx.Any(f => (f.ToUpper() == fileName.ToUpper()))) ||
  49.                 (filePatternRegex != null) && (filePatternRegex.IsMatch(fileName)));
  50.         if (ieCacheDeleteRule == SPVSProjectProps.IECacheDeleteRules.OnlyCurrentSite)
  51.         {
  52.             result = result && isFromCurrentSite;
  53.         }
  54.     }
  55.  
  56.     if ((result) && (!filesDeleted.Contains(uri.AbsoluteUri)))
  57.     {
  58.         filesDeleted.Add(uri.AbsoluteUri);
  59.         LogToOutputWindows(project, String.Format("Deleting file from IE cache: '{0}'", uri));
  60.     }
  61.  
  62.     return result;
  63. }

I kept the DeleteIECache class and its ClearIEFiles method as it was published on MSDN, except this method has a Func<Uri, bool> shouldDelete parameter that we use to decide if a specific file should be deleted from the IE cache:

returnValue = shouldDelete(uri) ? DeleteUrlCacheEntry(internetCacheEntry.lpszSourceUrlName) : false;

The code for the deployment step is pretty straightforward, it simply calls the ClearIECacheFile method of our ExtensionHelper class. Since we don’t have to call any SharePoint-specific code on the server side (that means no x64 process), there is no need for SharePoint commands in this case. That makes our life easier.

Code Snippet
  1. [DeploymentStep("PHolpar.ClearIECache")]
  2. [Export(typeof(IDeploymentStep))]
  3. internal class ClearIECacheDeployStep : IDeploymentStep
  4. {
  5.  
  6.     public bool CanExecute(IDeploymentContext context)
  7.     {
  8.         return true;
  9.     }
  10.  
  11.     public void Execute(IDeploymentContext context)
  12.     {
  13.         ISharePointProject project = context.Project;
  14.         ExtensionHelper.ClearIECacheFile(project);
  15.     }
  16.  
  17.     public void Initialize(IDeploymentStepInfo stepInfo)
  18.     {
  19.         stepInfo.Name = "Clear IE cache";
  20.         stepInfo.Description = "This step deletes the specified files from the Internet Explorer cache of the current user.";
  21.         stepInfo.StatusBarMessage = "IE cache is being cleared…";
  22.     }
  23. }

To test the new extension, I’ve created a deployment configuration called Clear IE cache.

image

This DC has only a single deployment step, our new Clear IE cache DS.

image

Using the following project properties (reg. exp. used: ^.*\.(js|JS)$) we can delete all .js files that were cached from the active SharePoint site:

image

We can specify the files to be deleted explicitly in the IE cache file list property:

image

In this case these files would be deleted from the cache independently from the URL they were downloaded from:

image

The next screenshot displays a sample deployment for the previous configuration:

image

Using the All from current sites value of IE cache delete rule (not illustrated here) the deployment process clears all files cached from the active SharePoint site, filtering properties (IE cache file list and IE cache file pattern) are ignored.

You can download the updated version (v 1.1) of my VS extension from the original location.

February 26, 2013

Using properties of the current SharePoint list when working with JavaScript

Filed under: JavaScript, SP 2010, Tips & Tricks — Tags: , , — Peter Holpar @ 13:04

Roughly 1,5 years ago I wrote a post about how we can use the SP.ListOperation.Selection object and its getSelectedList method to find out the ID of the current list, and how to submit this value as an asynchronous query using the ECMAScript Client Object Model to get other list properties, like Title. Although this method is probably still adequate for more advanced scenarios, there seems to be a way to get simple properties (like Title) easier, without a second round-trip to the server.

The key to the success is the non-documented GetCurrentCtx function. The next code snippet shows a few interesting properties available in this context.

Code Snippet
  1. var ctxT = GetCurrentCtx();
  2. // SPList.BaseType property (SPBaseType enumeration)
  3. alert(ctxT.listBaseType);
  4. // SPList.ID property
  5. alert(ctxT.listName);
  6. // SPList.BaseTemplate property (SPListTemplateType enumeration)
  7. alert(ctxT.listTemplate);
  8. // SPList.RootFolder.Url property
  9. alert(ctxT.listUrlDir);
  10. // SPList.Title property
  11. alert(ctxT.ListTitle);
  12. if (ctxT.ListTitle == 'YourListName') {
  13.     // do the custom action
  14. }

Note: Because this function is defined in CORE.JS, you should delay your script (for example, using SP.SOD.executeOrDelayUntilScriptLoaded), while the declaring script is fully loaded.

Using these properties is much easier and faster, than bothering with asynchronous Client OM requests. If you need to use other list / web / site properties, you should first check the source code of the page to decide if that property is available in the context. If it is not included in the page, you can fall back to the Client OM.

January 8, 2013

Using IE as a local Host for SharePoint ECMAScript Client Object Model

In the recent weeks I got an idea of a specific client-side HTML application (HTML file located on the local hard drive) that communicate with the SharePoint server. More on that idea later, as I hopefully achieve some results, but for now I would like to share a few interesting byproducts of my research.

This time I start with a few questions: what can we do, if we need a client application that manipulates SharePoint objects on a computer that doesn’t have (or can’t have) the managed client object model installed on it? Let’s say, you don’t have even the necessary .NET runtime version, or you don’t have Visual Studio, you have no former C# / PowerShell experience (or at least, you would not like to bother with them as you need a quickly editable application), so assume all you know is HTML and the good old JavaScript. You can’t deploy files to the SharePoint site, you don’t have even page edit permissions, so the standard way of injecting JavaScript through the Content Editor Web Part (CEWP) is not available to you. Using the REST API from your HTML pages through web requests could be an option, but it might be simply not powerful enough (especially in SharePoint 2010 version) to achieve your goals.

Using the ECMAScript Client Object Model (aka JavaScript Client Object Model or JSCOM) would be ideal to this task, but it has a significant out-of-the-box limitation: it was designed to work in the SharePoint site context it interacts with, that means, you can use only relative URLs when instantiating the SP.ClientContext object, or get the current context from the SharePoint page the script is used on, none of these is possible when working with a local HTML file.

In the last years I already delved quite deep into the client object model (both the managed and the JavaScript versions), and learned that even the out-of-the-box asynchronous calls can be altered to synchronous, so we should never give up.

In this post I show you a possible workaround for the problem in the case of an on-premise SharePoint 2010 that authenticates users through standard windows-integrated authentication. In the next post I plan to illustrate an even more complex solution for the case of the Office 365 Preview (that is a cloud-based SharePoint 2013).

As always, Fiddler proved again to be an invaluable tool analyzing the network traffic caused by the managed OM and trying to simulate the same for ECMAScript.

NOTE: As in the case of the former hacks, be aware that the methods described here don’t use the public, documented interfaces, so it is probably not supported by MS, and suggested to be used only as an experiment. There is no guarantee that it will work for you, especially if our environments are at different SP / cumulative update level.

In the progress of my work, I faced two major (and several minor) issues:

1. How to enforce using of absolute site URLs in JSCOM instead of the relative ones? It was the easier problem to solve as you see it soon.

2. How to achieve the right request digest token to get authorization to access the server side object from the JavaScript code.

Prerequisites to start the work with JSCOM in the local HTML files:

  • References to the SharePoint JavaScript libraries (and jQuery if you would like to work with that). These ones are included “automatically” when working with standard SharePoint pages, but in this case we should reference them (in the correct order!) as well. We could start our custom scripts only after our page and these libraries are already loaded.
  • A defined JavaScript variable (see more about that below) called _spPageContextInfo. That is in our case only a dummy placeholder (can be copied from the source of a SharePoint page) that the scripts depend on.
  • A HTML input field (by default it is a hidden one) called __REQUESTDIGEST that contains the authentication digest for the page. It can be copied from the source of a SharePoint page as well, but it has an expiration time. It is no problem in the case of a web page, as it is refreshed on the server site on page reloads, but it is not the case in the local file, so we should get always an actual one (see problem 2 above).

Regarding Issue 1: After a short investigation I found that the URL the client requests are sent to is determined by the $1P_0 property of the SP.ClientContext object. We should set our custom value right after initializing the context.

var siteRelativeUrl = "/";
var siteFullUrl = "http://intranet.contoso.com&quot;;
var context = new SP.ClientContext(siteRelativeUrl); // we set a dummy relative path that always exists
context.$1P_0 = siteFullUrl;

Setting the absolute URL is not enough for the successful request. Next step was to solve the issue of the request digest as without that our request would be rejected. It took me a while, but at the end it turned to be the same token as the one returned by the GetUpdatedFormDigestInformation method of the Sites web service. So all we have to do is to call this method with the properly formatted request, and replace the value of the __REQUESTDIGEST field before calling the actual JSCOM code.

On of the minor issues was, how to send POST requests to another domain (issue with cross-domain scripting), in this case to access the Sites web service. For GET requests there is JSONP (you can set crossDomain: true for your AJAX request in jQuery), but for POST, it is not available, crossDomain had simply no effect during my tests. In this situation I received an “Invalid argument.” error thrown in MicrosoftAjax.js when setting headers for the web requests.

Fortunately I found this jQuery option that solved this problem for me:

$.support.cors = true;

NOTE: When you open your HTML page not from the file system, but rather through another web server, you might be faced with the following prompt:

image

Choosing the default value (that is “No”) results in an “Access is denied.” error in MicrosoftAjax.js.

NOTE: For the sake of simplicity, I’ve uploaded a jQuery version (v1.8.2 to be exact, as the version number may be important) to my SharePoint server. In the real life one can reference for example http://code.jquery.com/jquery-latest.min.js instead of this.

Finally, here is the full source code of a “minimized” page that displays the number and the name of the lists on a remote SharePoint site. It is assumed that you are online (access to SP) and are (or can be) authenticated by the SharePoint server, having minimum read permissions on the site:

Code Snippet
  1. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/jquery/jquery.min.js"></script>
  2. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/1033/init.js"></script>
  3. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/MicrosoftAjax.js"></script>
  4. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/CUI.js"></script>
  5. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/1033/Core.js"></script>
  6. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/SP.Core.js"></script>
  7. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/SP.Runtime.js"></script>
  8. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/SP.js"></script>
  9.  
  10. <input type="hidden" name="__REQUESTDIGEST" id="__REQUESTDIGEST" value="0x0B5280FBC219C9A7E41746D9EA6AC24E65CB936560A6856AA4C38997F114401AED3254D7DDFACBE03C2028F689D0E67F55239E8FA679CE15F3EBC7A0C6280A34,05 Jan 2013 21:34:32 -0000" />
  11.  
  12. <script language="ecmascript" type="text/ecmascript">
  13.  
  14.     // define token request XML
  15.     var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
  16.     tokenReq += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:xsd="http://www.w3.org/2001/XMLSchema&quot; xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">&#039;;
  17.     tokenReq += '  <soap:Body>';
  18.     tokenReq += '    <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/&quot; />';
  19.     tokenReq += '  </soap:Body>';
  20.     tokenReq += '</soap:Envelope>';
  21.  
  22.     var siteFullUrl = "http://intranet.contoso.com&quot;;
  23.     var lists;
  24.  
  25.     function startScript() {
  26.       refreshDigest();
  27.     }
  28.  
  29.     function refreshDigest() {
  30.         $.support.cors = true; // enable cross-domain query
  31.         $.ajax({
  32.             type: 'POST',
  33.             data: tokenReq,
  34.             crossDomain: true,  // had no effect, see support.cors above
  35.             contentType: 'text/xml; charset="utf-8"',
  36.             url: siteFullUrl + '/_vti_bin/sites.asmx',
  37.             headers: {
  38.                 'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation&#039;,
  39.                 'X-RequestForceAuthentication': 'true'
  40.             },   
  41.             dataType: 'xml',
  42.             complete: function (result) {  
  43.                 $('#__REQUESTDIGEST').val($(result.responseXML).find("DigestValue").text());
  44.                 sendJSCOMReq();
  45.             }
  46.         });
  47.     }
  48.  
  49.     function sendJSCOMReq()
  50.     {
  51.         var _spPageContextInfo = {webServerRelativeUrl: "\u002f", webLanguage: 1033, currentLanguage: 1033, webUIVersion:4,pageListId:"{7bbd4c55-f832-40e2-8e2a-243455c3b2ba}",pageItemId:1,userId:1073741823, alertsEnabled:true, siteServerRelativeUrl: "\u002f", allowSilverlightPrompt:'True'};
  52.  
  53.         var siteRelativeUrl = "/";
  54.         var context = new SP.ClientContext(siteRelativeUrl); // we set a dummy relative path that always exists
  55.         context.$1P_0 = siteFullUrl;
  56.  
  57.         var web = context.get_web();
  58.         lists = web.get_lists();
  59.  
  60.         context.load(lists);
  61.         context.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));        
  62.     }
  63.  
  64.     function onQuerySucceeded(sender, args) {
  65.         var count = lists.get_count();
  66.         var listTitles = "Number of lists: " + count + ":\n";
  67.         for(var i=0;i<count; i++)
  68.         {
  69.             var list = lists.get_item(i);
  70.             listTitles += "  " + list.get_title() + "\n";
  71.         }
  72.         alert(listTitles);
  73.     }
  74.  
  75.     function onQueryFailed(sender, args) {
  76.       alert("Request failed: "+ args.get_message());
  77.     }
  78.  
  79.     // start the custom script execution after the scripts and page are loaded
  80.     SP.SOD.executeOrDelayUntilScriptLoaded(function () {
  81.         $(document).ready(startScript);
  82.     }, "sp.js");
  83.  
  84. </script>

After solving these problems I had a bad feeling. What happens if I authenticate myself as a standard user with low privileges, then stole the digest of a site owner included in a SharePoint page using a network traffic analyzer tool and try to send my request with the token of the other user? My experiments show that this issue is handled by SharePoint, as I received an error stating the token was not valid and I had to get a new one from the server.

In the next post I take another step forward to show you how to achieve the same using the Office 365 Preview version.

November 25, 2012

How to reduce the number of requests sent to the server when working with the Client Object Model?

Filed under: JSCOM, Managed Client OM, SP 2010 — Tags: , , — Peter Holpar @ 21:27

Assume you need to access data on SharePoint server. The information you need consists of a batch of similar items, for example, properties (like name) of users having their ID from a specific array, or the title of sites having their URL from an array as well. In the case of users (or other entities represented via list items) you could use CAML (see my former post for a similar, simple JSCOM example), although the query itself would be rather complex after a few dozens of items, however, other cases (like site names) might raise more serious issues.

You could send the requests consecutively, sending the next request only after you received the response for the former one, or you can send a set of asynchronous requests one after the other, and then process the responses parallel. I found neither of these options to be ideal. On one side, the high number of roundtrips between the client and server won’t help performance, on the other side, synchronizing requests and responses (have we received all the responses for our requests?) could be a nightmare.

Instead of these ones, we can implement a solution, where we cumulate all of the requests into a single context, then send the query at once, and process the single response.

Below I provide an example of this implementation for the managed client object model:

  1. List<int> lookupids = new List<int> { 19, 23, 32 };
  2. List<ListItem> userInfos = new List<ListItem>();
  3. using (ClientContext clientContext = new ClientContext("http://yoursharepointsite&quot;))
  4. {
  5.     lookupids.ForEach(id =>
  6.         {
  7.             ListItem userInfo = clientContext.Web.SiteUserInfoList.GetItemById(id);
  8.             userInfos.Add(userInfo);
  9.             clientContext.Load(userInfo);
  10.         });
  11.     clientContext.ExecuteQuery();
  12.     IEnumerable<string> userEmails = userInfos.Select(userInfo => userInfo["EMail"].ToString());
  13.     Console.WriteLine(String.Join(", ", userEmails.ToArray()));
  14. }

and the JavaScript version as well:

  1. <script type="text/javascript">
  2.  
  3. var siteUrl = '/';
  4. var userInfos;
  5.  
  6. function retrieveUsers() {
  7.     var ids = [19, 23, 32];
  8.     userInfos = new Array();
  9.  
  10.     var clientContext = new SP.ClientContext(siteUrl);
  11.     var web = clientContext.get_web();
  12.     var userList = web.get_siteUserInfoList();
  13.  
  14.     for(i=0; i<ids.length; i++) {
  15.         var id = ids[i];
  16.         var userInfo = userList.getItemById(id);
  17.         clientContext.load(userInfo);
  18.         userInfos[i] = userInfo;
  19.     }
  20.  
  21.     clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));        
  22. }
  23.  
  24. function onQuerySucceeded(sender, args) {  
  25.     var result = '';
  26.     for(i=0; i<userInfos.length; i++)
  27.     {
  28.         var userInfo = userInfos[i];
  29.         result += userInfo.get_item('Name') + ', ' + userInfo.get_item('Title') + ', ' + userInfo.get_item('EMail') + '\n';
  30.     }  
  31.     alert(result);
  32. }
  33.  
  34. function onQueryFailed(sender, args) {
  35.  
  36.     alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  37. }
  38.  
  39. ExecuteOrDelayUntilScriptLoaded(retrieveListItems, "sp.ribbon.js");
  40.  
  41. </script>

Note: The key factor of this solution is that you subsequent queries must be independent from the responses of the former requests.

How to get the users from a multivalued field using JSCOM?

Filed under: JavaScript, JSCOM, SP 2010 — Tags: , , — Peter Holpar @ 21:17

Assume you have a SharePoint list called Projects with a field called Members of type Person or Group and the option Allow Multiple Selections is enabled. You are to retrieve the users from the Members field using JavaScript Client Object Model (JSCOM).

Note: A similar problem and solution for the Managed Client Object Model can be found here.

In this case the value of the Members field is an instance of SPFieldUserValueCollection on the server side, on the client side it’s an array of FieldUserValue instances. This latter type is a special case of the lookup value, inherited from the FieldLookupValue type, so we can access its members similarly, that means, we should use the get_lookupId and get_lookupValue methods.

In our case the JavaScript code to access the members of a specific project looks like this:

  1. <script type="text/javascript">
  2.  
  3. var siteUrl = '/';
  4.  
  5. function retrieveListItems() {
  6.  
  7.     var clientContext = new SP.ClientContext(siteUrl);
  8.     var projectList = clientContext.get_web().get_lists().getByTitle('Projects');
  9.         
  10.     // instead of the simple CAML query we could use the getItemById method of the list object
  11.     // especially if we need a simple item only
  12.     // but if we need for example the top 5 items, the CAML way is easier
  13.     var camlQuery = new SP.CamlQuery();
  14.     camlQuery.set_viewXml('<View><Query><Where><Geq><FieldRef Name=\'ID\'/><Value Type=\'Number\'>1</Value></Geq></Where></Query><RowLimit>5</RowLimit></View>');
  15.     this.collListItem = projectList.getItems(camlQuery);
  16.         
  17.     clientContext.load(collListItem);
  18.         
  19.     clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));        
  20.         
  21. }
  22.  
  23. function onQuerySucceeded(sender, args) {
  24.     var listItemInfo = '';
  25.     var listItemEnumerator = collListItem.getEnumerator();
  26.     while (listItemEnumerator.moveNext()) {
  27.          var oListItem = listItemEnumerator.get_current();
  28.         var result = '';
  29.         var members = oListItem.get_item('Members');
  30.         for(i=0; i<members.length; i++)
  31.         {
  32.             var member = members[i];
  33.             result += member.get_lookupId() + ', ' + member.get_lookupValue() + '\n';
  34.         }  
  35.  
  36.        
  37.         listItemInfo += '\nID: ' + oListItem.get_id() +
  38.         '\nMembers: ' +  result +
  39.             '\nTitle: ' + oListItem.get_item('Title');
  40.     }
  41.  
  42.     alert(listItemInfo.toString());
  43. }
  44.  
  45. function onQueryFailed(sender, args) {
  46.  
  47.     alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  48. }
  49.  
  50. ExecuteOrDelayUntilScriptLoaded(retrieveListItems, "sp.ribbon.js");
  51.  
  52. </script>

Older Posts »

Theme: Shocking Blue Green. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 42 other followers