Archive

Posts Tagged ‘jQuery’

Knockout.js Validation: Check if value is in range

June 20th, 2015 1 comment

    To validate a model and its properties (observable and ordinary ones) we use such knockout.js plugin as Knockout Validation. This validation library is simple and easy to extend. For example, a new validation rule to check if value is in range could be as simple as the following:

import ko = require("knockout");
import validation = require("knockout.validation");
...
export function enableCustomValidators() {

 validation.rules['inRange'] = {
  validator: function (val, params) {
   var minVal = ko.validation.utils.getValue(params.min);
   var maxVal = ko.validation.utils.getValue(params.max);

   var res = params.includeMin ? val >= minVal : val > minVal;

   if (res)
    res = params.includeMax ? val <= maxVal : val < maxVal;

   if (!res)
    this.message = ko.validation.formatMessage(params.messageTemplate || this.messageTemplate, 
       [minVal, maxVal, (params.includeMin ? '[' : '('), (params.includeMax ? ']' : ')') ]);

   return res;
   },
   messageTemplate: 'Value has to be in range {2}{0}, {1}{3}'
 };

 validation.registerExtenders(); // adds validation rules as extenders to ko.extenders
}
...
// the enableCustomValidators should be called when the custom 
// validation rules are supposed to be used

Note the code above and further is TypeScript. The TypeScript definition to the Knockout Validation is available as a NuGet package, namely knockout.validation.TypeScript.DefinitelyTyped.

The inRange rule accepts the start and end values and allows to indicate should or should not they be included in the range (by default boundaries are excluded). The validation error message is being generated based on the custom defined messageTemplate or default one. The default message template uses four placeholders where the {0} and {1} are start and end values correspondingly. The latter two represent brackets: square or round ones depending on the boundaries inclusion.

The example below defines inRange validation to keep the value between 0 (including it) and 10 (excluding it):

public someNumber = ko.observable<number>(null).extend(
{
	required: { params: true, message: "This field is required" },
	number:   { params: true, message: "Must be a number" },
	
	inRange:  {
		params: {
			min: 0,
			includeMin: true,
			max: 10,
			//includeMax: true,
		}
	}
});

In case of a wrong value the message will says “Value has to be in range [0, 10)“.

To redefine the message template the following might be used:

public somePositiveNumber = ko.observable<number>(null).extend(
{
 required: { params: true, message: "This field is required" },
 number:   { params: true, message: "Must be a number" },
	
 inRange:  {
  params: {
   min: 0,			
   max: 1,
   messageTemplate: "Must be a positive number less than {1}"// the range end value will be put
  }
 }
});

Note the ko.validation.formatMessage has been used to generate the final validation message. Till recently the ko.validation.formatMessage wasn’t able to process more than one placeholder in template. So, if you use one of such outdated versions of Knockout Validation library, you may be interested in the actual formatting method implementation shown below. It’s borrowed from the same Knockout Validation, but of up-to-date version.

function formatMessage(message, params, observable?) {
    if (ko.validation.utils.isObject(params) && params.typeAttr) {
        params = params.value;
    }
    if (typeof message === 'function') {
        return message(params, observable);
    }
    var replacements = ko.utils.unwrapObservable(params);
    if (replacements == null) {
        replacements = [];
    }
    if (!ko.validation.utils.isArray(replacements)) {
        replacements = [replacements];
    }
    return message.replace(/{(\d+)}/gi, function (match, index) {
        if (typeof replacements[index] !== 'undefined') {
            return replacements[index];
        }
        return match;
    });
}

Select2: jqModal + Select2 = can’t type in

June 10th, 2015 No comments

    The Select2 control placed in the jqModal modal dialog doesn’t allow typing anything in. It’s unexpected, but explainable: jqModal eliminates all keypress and keydown events coming from without, i.e. coming from the controls not residing in the jqModal container. As I mentioned in the Select2: Make dropdown vertically wider, the Select2, in turn, dynamically appends the elements making up the dropdown (including the type-ahead search box) to the Body-tag, so they are literally out of jqModal dialog.

Select2 in jqModal - No Typing in

Where the contracted jqModal markup resembles the following:

<div id="myModalDialog" class="popup-6"...>
    <div class="pp6-header">
        <h3>Some Fancy Modal Dialog</h3>
        <span class="close" ...></span>
    </div>
    <div class="pp6-body" ...>
        
        <select id="mySelect2" ...>
			<option value="">Select an option</option>
			<option value="Product">Product</option>
			<option value="Producer">Producer</option>
			...
		</select>
			
    </div>
    <div class="pp6-footer">
        <a href="javascript: void(0)" class="ok" ...>OK</a>
        <a href="javascript: void(0)" class="close" ...>Cancel</a>
    </div>    
</div>

The Select2 and modal dialog are created as follows:

	$("#mySelect2").select2();
	...
	$("#myModalDialog").jqm({
            modal: true,
            trigger: false,
            onHide: function (h) {
                // hash object;
                //  w: (jQuery object) The modal element
                //  c: (object) The modal's options object 
                //  o: (jQuery object) The overlay element
                //  t: (DOM object) The triggering element                
                h.o.remove(); // remove overlay
                h.w.hide(); // hide window      
            },
            onShow: function (hash) {                
                hash.o.addClass("modalBackground").prependTo("body"); // add overlay
				
				// getNextDialogZIndex is my custom function, 
				// which increments z-index counter to place each new dialog atop the preceding one
                if (typeof getNextDialogZIndex === "function") {
                    var zIndex = getNextDialogZIndex();
                    hash.o.css("z-index", zIndex);
                    hash.w.css("z-index", zIndex + 1);
                }
                hash.w.css("left", 0);
                hash.w.css("right", 0);
                hash.w.css("position", "fixed");
                hash.w.css("margin-left", "auto");
                hash.w.css("margin-right", "auto");                
                hash.w.fadeIn();//show();
            }
        });		
	...
	$("#myModalDialog").jqmShow();

Below I quote the code from jqModal.js where the keypress, keydown, mousedown events are being handled, and their bubbling is being interrupted

F = function(t){
	// F: The Keep Focus Function (for modal: true dialos)
	// Binds or Unbinds (t) the Focus Examination Function (X) to keypresses and clicks
		
	$(document)[t]("keypress keydown mousedown",X);		
		
}, X = function(e){
	// X: The Focus Examination Function (for modal: true dialogs)

	var modal = $(e.target).data('jqm') || $(e.target).parents('.jqm-init:first').data('jqm'),
		activeModal = A[A.length-1].data('jqm');
		
	// allow bubbling if event target is within active modal dialog
	if(modal && modal.ID == activeModal.ID) return true; 

	// else, trigger focusFunc (focus on first input element and halt bubbling)
	return $.jqm.focusFunc(activeModal);
}

The code examines if the event target control or its first found parent marked with the ‘jqm-init‘ class have the associated datajqm‘. If so, the event will be allowed to pass through, otherwise it will be ignored.

Based on that, we can apply a workaround to Select2 control to unlock the type-ahead search box for typing in. The solution implies to associate the proper data named ‘jqm‘ with the Select2‘s dropdown. To catch the right dropdown (we don’t want to affect other Select2 controls on the page), a unique css class has to be applied to it. Plus the dropdown has to have the ‘jqm-init‘ class to be considered and analyzed by the jqModal code (note the following fragment in the listing above:)

$(e.target).parents('.jqm-init:first')

So, the solution should look like

// create Select2 and apply proper css classes to its dropdown
$("#mySelect2").select2({ dropdownCssClass: "someUniqueCssClass jqm-init" });

// capture the data associated with the jqModal container
var d = $("#myModalDialog").data("jqm");

// associate the captured data with the dropdown 
// to mimic the residence within the jqModal dialog
$(".someUniqueCssClass.jqm-init").data('jqm', d);

After that every keypress, keydown or mousedown event won’t be suppressed, but will be properly handled.

Select2: Make dropdown vertically wider

June 1st, 2015 No comments

    The Select2 is a quite useful and fancy replacement for ordinary Html Selects. Having a fixed number of items available to pick, you may want to make the dropdown of a Select2 control vertically wider to avoid extra scrolling.

Before - Select2 With Scrolling

Where the Select2 control is defined as follows

<select name="criteria" id="criteria">
	<option value="">Select a Search Criteria</option>
	<option value="Product">Product</option>
	<option value="Producer">Producer</option>
	<option value="Store">Store</option>
	<option value="State">State</option>
	<option value="Country">Country</option>
	<option value="Continent">Continent</option>
	<option value="Planet">Planet</option>
	<option value="Galaxy">Galaxy</option>
	<option value="Universe">Universe</option>
	<option value="Reality">Reality</option>
</select>
	$("#criteria").select2({
		minimumResultsForSearch: -1 // allows to hide the type-ahead search box,
		                            // so the Select2 acts as a regular dropdown list
	});

Whenever the Select2 has been clicked, the dropdown-elements will be dynamically added to the DOM at the end of the body-tag and will pop up. To make the dropdown wider we need to apply the proper ccs style to it. Of course, we can change the default css styles accompanying the Select2 control, but there is a way to apply desired changes to a certain control, not affecting other Select2 controls on the page. The custom css class can be attached to the target dropdown by the following call:

	$("#criteria").select2({
		minimumResultsForSearch: -1,
		dropdownCssClass: "verticallyWider" // the dropdownCssClass option is intended 
                                            // to specify a custom dropdown css class
	});                        

The Html markup dynamically added to the DOM when clicking the Select2 resembles the following (note the verticallyWider class, which among others has been applied to the outer div-tag):

<div class="select2-drop-mask" id="select2-drop-mask"></div>

<div class="select2-drop select2-display-none verticallyWider select2-drop-active" 
	id="select2-drop" 
	style="left: 828.49px; top: 112.61px; width: 300px; bottom: auto; display: block;">
	<div class="select2-search select2-search-hidden select2-offscreen">
		<label class="select2-offscreen" for="s2id_autogen2_search"></label>
		<input class="select2-input" id="s2id_autogen2_search" role="combobox" 
			aria-expanded="true" aria-activedescendant="select2-result-label-8" 
			aria-owns="select2-results-2" spellcheck="false" aria-autocomplete="list" 
			type="text" placeholder="" autocapitalize="off" 
			autocorrect="off" autocomplete="off" />
	</div>
   <ul class="select2-results" id="select2-results-2" role="listbox">
      <li class="select2-results-dept-0 select2-result select2-result-selectable" 
		role="presentation">
         <div class="select2-result-label" id="select2-result-label-3" role="option">
			<span class="select2-match"></span>Select a Search Criteria
		 </div>
      </li>
      <li class="select2-results-dept-0 select2-result select2-result-selectable" 
		role="presentation">
         <div class="select2-result-label" id="select2-result-label-4" role="option">
			<span class="select2-match"></span>Product
		 </div>
      </li>
      <li class="select2-results-dept-0 select2-result select2-result-selectable" 
		role="presentation">
         <div class="select2-result-label" id="select2-result-label-5" role="option">
			<span class="select2-match"></span>Producer
		 </div>
      </li>
      ...
      <li class="select2-results-dept-0 select2-result select2-result-selectable" 
		role="presentation">
         <div class="select2-result-label" id="select2-result-label-13" role="option">
			<span class="select2-match"></span>Reality
		 </div>
      </li>
   </ul>
</div>

The verticallyWider class, in turn, is defined as the following (so, specify the desired min/max heights in the styles):

.verticallyWider.select2-container .select2-results {
    max-height: 400px;
}
.verticallyWider .select2-results {
    max-height: 400px;
}
.verticallyWider .select2-choices {
    min-height: 150px; max-height: 400px; overflow-y: auto;
}

Below is the result of the modifications

After - Select2 With No Scrolling - Vertically Wider

SharePoint: How to create a custom REST WCF Service available in _layouts folder

August 8th, 2013 No comments

    In the given blogpost I’ll describe step by step how to create a simple REST WCF Service residing in the _layouts folder. To create a demonstration project we’ll use Visual Studio 2012. I believe, however, the similar steps could be done in Visual Studio 2010 as well.

* Don’t look for a meaning in the source code of the demonstration project as its main goal is just to show how to communicate with WCF Services operating under SharePoint and how they return and accept parameters of standard and custom types.

* If you are too lazy to go through all steps, you can download the demonstration project right away and play with it 🙂

1. Create a SharePoint project in the Visual Studio 2012

* Note that if you plan to add the service to an existent project, just skip this step.

Click to see how to create the SharePoint project in details

  1. Go to FILE -> New -> Project and select the SharePoint 2010 Project available under Templates/Visial C#/SharePoint (see the image below). Type the name and location of the project. Let’s name it CustomRestService.
    Create SharePoint Project In Visual Studio 2012
  2. Click OK button, then the SharePoint Customization Wizard opens (see the image below). Type the proper URL of the local site for debugging and choose the Deploy as a farm solution option.
    SharePoint Customization Wizard
  3. Click Finish button. In Solution Explorer the created project resembles the following:
    SharePoint Project After Creation


2. Add a WCF Service to the project

Click to see how to add WCF Service to the project in details

  1. Ensure that the CustomRestService project is selected in Solution Explorer and then go to PROJECT -> Add New Item. In the opened dialog, locate the WCF Service among available templates. It’s usually under the Visual C# Items. Alternatively you can find the required template by typing WCF in the search box (Search Installed Templates) at the right top corner (see the picture below). Let’s name the service CustomRestService as well.
    Add WCF Service to the project
  2. Click Add button. After the WCF Service has been added, the project should look like the following:
    SharePoint Project After WCF Service has been added
    There are 3 new files in the project:

    • ICustomRestService.cs, defines the interface supported by the service, i.e. specifies the methods available in the service;
    • CustomRestService.cs, provides the actual implementation of the service’s methods;
    • app.config, presets the WCF Service configuration;

    Delete the app.config from the project as we don’t actually need it.
    * If you don’t have the WCF Service template available in Visual Studio or you don’t want to use it for some reason, you can just add two .cs files, ICustomRestService.cs and CustomRestService.cs, to the project. In this case don’t forget to add references to such assemblies as System.Runtime.Serialization and System.ServiceModel.


3. Add references to System.ServiceModel.Web and System.Web assemblies to the project

Click to see how to add references to the project in details

  1. Ensure the CustomRestService project is selected in Solution Explorer and then go to PROJECT -> Add Reference…. In the opened dialog, find the System.ServiceModel.Web and System.Web assemblies (usually they are located under Assemblies/Framework) and check the checkboxs against them (see the picture below).
    Add References to System.ServiceModel.Web and System.Web to the project
  2. Click OK button. The References at that stage for the project created from scratch should look like the following:
    References After Adding System.ServiceModel.Web and System.Web Assemblies
    The highlighted assemblies are essential for developing WCF Services operating under SharePoint.


4. Modify the service’s interface

Open the ICustomRestService.cs and bring it to the following state:

using System.ServiceModel;
using System.ServiceModel.Web;

namespace CustomRestService
{
    /// <summary>
    /// Wraps the service result and provides the error message (if any)
    /// </summary>
    public class ServiceResult<T>
    {
        public bool   Success      { get; set; }
        public string ErrorMessage { get; set; }
        public T      Data         { get; set; }
    }

    /// <summary>
    /// Represents basic info about a book
    /// </summary>
    public class Book
    {
        public int    Id     { get; set; }
        public string Title  { get; set; }
        public string Author { get; set; }
    }

    /// <summary>
    /// Service's interface
    /// </summary>
    [ServiceContract]
    public interface ICustomRestService
    {
        /// <summary>
        /// Checks if book is already registered in the system
        /// </summary>
        [OperationContract]
        [WebInvoke(Method  = "POST", 
            RequestFormat  = WebMessageFormat.Json, 
            ResponseFormat = WebMessageFormat.Json, 
            BodyStyle      = WebMessageBodyStyle.WrappedRequest)]
        ServiceResult<bool> BookExists(string bookTitle);

        /// <summary>
        /// Returns book by id
        /// </summary>
        [OperationContract]
        [WebInvoke(Method  = "POST", 
            RequestFormat  = WebMessageFormat.Json, 
            ResponseFormat = WebMessageFormat.Json, 
            BodyStyle      = WebMessageBodyStyle.WrappedRequest)]
        ServiceResult<Book> GetBook(int id);

        /// <summary>
        /// Adds the passed book to the system
        /// </summary>
        [OperationContract]
        [WebInvoke(Method  = "POST",
            RequestFormat  = WebMessageFormat.Json,
            ResponseFormat = WebMessageFormat.Json,
            BodyStyle      = WebMessageBodyStyle.WrappedRequest)]
        ServiceResult<int> AddBook(Book book);
    }
}

Here the ServiceResult is a simple class that wraps the service result and provides the error message in case it occurs. The Book class provides basic info about a book and is intended to show how REST WCF Service deals with custom types.

5. Modify the service implementation

Open the CustomRestService.cs and modify it so that it looks like the following:

using System;
using System.ServiceModel.Activation;
using System.Web;
using Microsoft.SharePoint;

namespace CustomRestService
{
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class CustomRestService : ICustomRestService
    {
        public ServiceResult<bool> BookExists(string bookTitle)
        {
            // allow access for anonymous users

            if (string.IsNullOrEmpty(bookTitle))
                return new ServiceResult<bool>()
                { 
                    Success      = false, 
                    ErrorMessage = "Book title is undefined!" 
                };

            var result = new ServiceResult<bool>() { Success = true };

            try
            {
                SPList spList = SPContext.Current.Web.Lists["Books"];

                SPQuery spQuery = new SPQuery
                {
                    Query = "<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>" + 
                        bookTitle.ToLower() + "</Value></Eq></Where>",
                    ViewAttributes = "Scope='Recursive'"
                };

                SPListItemCollection books = spList.GetItems(spQuery);
                result.Data = books.Count > 0;
            }
            catch (Exception ex)
            {
                result.Success      = false;
                result.ErrorMessage = ex.Message;
            }

            return result;
        }

        public ServiceResult<Book> GetBook(int id)
        {
            // check if user is authenticated
            if (!HttpContext.Current.Request.IsAuthenticated)
                return new ServiceResult<Book>()
                    {
                        Success      = false, 
                        ErrorMessage = "Unauthenticated access!"
                    };

            if (id <= 0)
                return new ServiceResult<Book>()
                    {
                        Success      = false, 
                        ErrorMessage = "Invalid book id!"
                    };

            var result = new ServiceResult<Book>() { Success = true };

            try
            {
                SPList spList = SPContext.Current.Web.Lists["Books"];

                SPListItem spListItem = spList.GetItemById(id);
                result.Data = new Book()
                {
                    Id     = spListItem.ID, 
                    Title  = spListItem.Title, 
                    Author = Convert.ToString(spListItem["BookAuthor"])
                };
            }
            catch (Exception ex)
            {
                result.Success      = false;
                result.ErrorMessage = ex.Message;
            }

            return result;
        }

        public ServiceResult<int> AddBook(Book book)
        {
            // check if user is authenticated
            if (!HttpContext.Current.Request.IsAuthenticated)
                return new ServiceResult<int>()
                    {
                        Success      = false, 
                        ErrorMessage = "Unauthenticated access!"
                    };

            if (book == null)
                return new ServiceResult<int>()
                    {
                        Success      = false, 
                        ErrorMessage = "Invalid book!"
                    };

            // checking if the book is already presented is omitted

            var result = new ServiceResult<int>() { Success = true };

            try
            {
                SPList spList = SPContext.Current.Web.Lists["Books"];

                SPListItem spListItem = spList.AddItem();
                spListItem["Title"]      = book.Title;
                spListItem["BookAuthor"] = book.Author;

                SPContext.Current.Web.AllowUnsafeUpdates = true;
                spListItem.Update();
                SPContext.Current.Web.AllowUnsafeUpdates = false;

                result.Data = spListItem.ID;
            }
            catch (Exception ex)
            {
                result.Success      = false;
                result.ErrorMessage = ex.Message;
            }

            return result;                
        }
    }
}

The implementation operates with the Books list and assumes that the list is available at the SharePoint Site in the boundaries of which the service is called. Note that we work with SharePoint objects in the same way as we would do this in code-behind of an .aspx-page.

The GetBook and AddBook methods require users to be authenticated before calling them, while the BookExists allows anonymous access (of course, in case it’s turned on for Web Application and Site). Since it’s a best practice, always put the checking whether user is authenticated and authorized in the beginning of every web method unless you have some contrary requirements (for example, to provide anonymous access).

6. Add Layouts mapped folder to the project

* Note that if you already have the folder, just skip this step.

Click to see how to add “Layouts” mapped folder to the project in details

    Ensure that the CustomRestService project is selected in Solution Explorer and then go to PROJECT -> Add SharePoint “Layouts” Mapped Folder. The Layouts folder along with the nested CustomRestService folder will be added to the project. For simplicity rename the nested CustomRestService to Wcf. After that the project should look as follows:
The project after the Layouts mapped folder has been added


7. Add .svc file to the Layouts folder

Click to see how to add .svc file to the Layouts folder in details

  1. Ensure that the Wcf folder nested in the Layouts is selected in Solution Explorer and then go to PROJECT -> Add New Item. In the opened dialog, locate the Text File among available templates. It’s usually under the Visual C# Items/General (see the image below). Name the file CustomRestService.svc.
    Add .svc file to the project
  2. Click Add button. After the .svc file has been added, the project should look like the following:
    The project after the .svc file has been added


8. Modify the .svc file

Open the CustomRestService.svc and bring it to the following:

<%@ ServiceHost Language="C#" Debug="true" 
    Service="CustomRestService.CustomRestService, $SharePoint.Project.AssemblyFullName$"
    CodeBehind="CustomRestService.cs"
    Factory="Microsoft.SharePoint.Client.Services.MultipleBaseAddressWebServiceHostFactory,
             Microsoft.SharePoint.Client.ServerRuntime, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

The MultipleBaseAddressWebServiceHostFactory declares the REST type of the Web Service and creates proper endpoints with Web Bindings. The factory takes upon itself the configuring of the Web Service, that is why we don’t need the app.config deleted previously.

The Service attribute provides the Fully Qualified Name (including the assembly’s full name) of the class representing the service. Instead of putting in the proper full name of the assembly I used such replaceable parameter as $SharePoint.Project.AssemblyFullName$. When building the project, Visual Studio is supposed to replace the parameter with the full name of the output assembly. One nuisance here is that Visual Studio doesn’t perform replacement in .svc files by default. To make Visual Studio look into .svc, we have to manually alter the .csproj. So, open the project’s .csproj file in Notepad or other Notepad-like text editor, find the first <PropertyGroup> node, insert the following before the closing </PropertyGroup> and save the file:

<TokenReplacementFileExtensions>svc</TokenReplacementFileExtensions>

* Note that, having modified the .csproj, you likely will be prompted by Visual Studio to reload the project.

After the alteration of the project’s file it should resemble the following (some tags and attributes are skipped for simplicity):

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" ...>
  <Import ... />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{16585D01-2995-48EF-8DFA-1371C9EF6EE9}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>CustomRestService</RootNamespace>
    <AssemblyName>CustomRestService</AssemblyName>
    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{BB1F664B-9266-4fd6-B973-E1E44974B511};...</ProjectTypeGuids>
    <SandboxedSolution>False</SandboxedSolution>
    <WcfConfigValidationEnabled>True</WcfConfigValidationEnabled>
	<TokenReplacementFileExtensions>svc</TokenReplacementFileExtensions>
  </PropertyGroup>
...
</Project>

* Note that for the project attached to the given article, the .csproj has been already modified.

Of course, instead of the $SharePoint.Project.AssemblyFullName$ you can use the real full name of your assembly. To get such name I usually use .Net Reflector. Alternatively you can make Visual Studio display the name, following instructions from the article How to: Create a Tool to Get the Full Name of an Assembly.

9. Build the project and deploy it

Right-click the CustomRestService project in Solution Explorer and then click Deploy. Behind the scenes, Visual Studio compiles the project, creates a .wsp file and deploys the package to the farm.

10. Test the service

  1. First of all try to open the service in a browser. You can use for that any SharePoint Site, just add the /_layouts/wcf/customrestservice.svc at the end of the URL. For example:
    http://someservername:1200/somesite/_layouts/wcf/customrestservice.svc
    

    If you see the message “Endpoint not found“, don’t worry, that’s what we need. That means all of the steps above have been done correctly.

  2. Let’s test the web methods of the service. As you remember, the service interacts with the Books list. It’s a list with two main fields: Title and BookAuthor, both are Single Line of Text (see the image below).
    Books SharePoint List
    * I do not cite the list’s schema here as you can find it (schema, instance, feature to create the instance) in the attached project.
    The test page named TestPage.aspx is added to the demonstration project as well. The page has been created as an Application Page, locates in the Layouts folder and contains one button that executes JavaScript functions to test each web method. Below is the shortened listing of the page to show how to call web methods using jQuery and Ajax:

    ...
    <%@ Page ... CodeBehind="TestPage.aspx.cs" 
             Inherits="CustomRestService.Layouts.TestPage" 
    		 DynamicMasterPageFile="~masterurl/default.master" %>
    
    <asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
     <script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
    
     <script type="text/javascript">
      // makes POST json-request to a web method
      var getJsonData = function (url, param, callback, errorcallback) {
        $.ajax({
    	  url        : url,
    	  dataType   : "json",
    	  type       : "POST",
    	  contentType: 'application/json; charset=utf-8',
    	  data       : JSON.stringify(param),
    	  success    : function (data) {
    		  if (callback)
    			  callback(data);
    	  },
    	  error      : function (XMLHttpRequest, textStatus, errorThrown) {
    		  if (errorcallback)
    			  errorcallback(XMLHttpRequest, textStatus, errorThrown);
    	  }
        });
      };
    
      // makes request to the BookExists method
      function testBookExists() {
        getJsonData('Wcf/CustomRestService.svc/BookExists', 
          { "bookTitle": 'the adventures of tom sawyer' },
      	  function (data) {
    		  if (data.Success)
    			  alert('Success. Result: ' + data.Data);
    		  else
    			  alert('Fail. Error: ' + data.ErrorMessage);
    	  },
    	  function(XMLHttpRequest, textStatus, errorThrown) {
    		  alert(errorThrown);
    	  });	
      }
    
      // makes request to the GetBook method
      function testGetBook() {
        getJsonData('Wcf/CustomRestService.svc/GetBook', { "id": 2 },
    	  function (data) {
    		  if (data.Success)
    			  alert('Success. Title: ' + data.Data.Title + ' Author: ' + data.Data.Author);
    		  else
    			  alert('Fail. Error: ' + data.ErrorMessage);
    	  },
    	  function (XMLHttpRequest, textStatus, errorThrown) {
    		  alert(errorThrown);
    	  });
      }
    
      // makes request to the AddBook method
      function testAddBook() {
        getJsonData('Wcf/CustomRestService.svc/AddBook', 
          { "book": { "Author": "Mark Twain", "Title": "The Adventures of Huckleberry Finn" } },
    	  function (data) {
    		  if (data.Success)
    			  alert('Success. Book Id: ' + data.Data);
    		  else
    			  alert('Fail. Error: ' + data.ErrorMessage);
    	  },
    	  function (XMLHttpRequest, textStatus, errorThrown) {
    		  alert(errorThrown);
    	  });
      }
     </script>
    </asp:Content>
    
    <asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
     <asp:Button ID="test" Text="Test Web Methods" runat="server" 
       OnClientClick="testBookExists(); testGetBook(); testAddBook(); return false;" />
    </asp:Content>
    ...
    

    Pay attention to the URLs used to send request to web methods. The given test page is located directly in _layouts, so to access the Web Service placed in the _layouts/Wcf I use the relative URL Wcf/CustomRestService.svc. If your page dealing with the Web Service resides somewhere outside of _layouts or within the folders nested in _layouts you have to provide the proper URL (relative or not) to the Web Service.
    Open the test page in a browser, the URL should be like the following

    http://someservername:1200/somesite/_layouts/TestPage.aspx
    

    Click Test Web Methods button. If everything works fine you’ll get 3 alert messages and each of them starts with the word “Success”. The order of the alerts may vary as requests are asynchronous.

The demonstration project you can download here – CustomRestService.zip. Note that the archive contains the .wsp package in the CustomRestService\bin subfolder, use it in case you are not able to compile/build the project for some reason.

Knockout.js: HtmlNoScript binding

February 14th, 2013 No comments

    The Knockout.js is one of the most popular and fast-developing libraries that bring the MVVM pattern into the world of JavaScript development. Knockout.js allows to declaratively bind UI (Document Object Model elements) to an underlying data model. One of the built-in bindings is the html-binding allowing to display an arbitrary Html provided by an associated property/field of the data model. The typical use of this binding may look like this:

<div data-bind="html: comment" />

where the comment property may be specified as shown in the following data model:

...
// include the knockout library
<script src="js/knockout/knockout-2.2.0.debug.js" type="text/javascript"></script>
...
$(function () {
	// define a data model
	var viewModel = function () {
        var self = this;		
		self.comment = ko.observable("some html comment <span>Hello<script type='text/javascript'>alert('Hello!');<\/script></span>");
		...
	}
	...
	// create instance of the data model
	var viewModelInstance = new viewModel();
	// bind the instance to UI (since that moment all of the declarative bindings have worked off and the data are displayed)
	ko.applyBindings(viewModelInstance);
});

Applying a value to a DOM element, the html-binding exploits the jQuery‘s html() method, of course, if jQuery is available on the page; otherwise knockout.js relies on its own logic that ends up with call of the DOM‘s appendChild function. In both cases if the html being applied contains inclusions of JavaScripts, the scripts execute once the html has been added to the DOM. Preventing all unwanted scripts from running is the best practice when displaying random htmls on the page. Using the approach described in my post How to prevent execution of JavaScript within a html being added to the DOM we are able to develop our own knockout binding disabling the scripts. It could look like the following:

 ko.bindingHandlers.htmlNoScripts = {
    init: function () {
        // Prevent binding on the dynamically-injected HTML
        return { 'controlsDescendantBindings': true };
    },
    update: function (element, valueAccessor, allBindingsAccessor) {
        // First get the latest data that we're bound to
        var value = valueAccessor();
        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.utils.unwrapObservable(value);
		// disable scripts
        var disarmedHtml = valueUnwrapped.replace(/<script(?=(\s|>))/i, '<script type="text/xml" ');
        ko.utils.setHtml(element, disarmedHtml);
    }
};
 

An alternative way to prevent scripts from running is to use the innerHTML property of a DOM element as follows:

 var html = "<script type='text/javascript'>alert('Hello!');<\/script>";
 var newSpan = document.createElement('span');
 newSpan.innerHTML = str;
 document.getElementById('content').appendChild(newSpan);
 

Some people, however, report that this doesn’t work in Internet Explorer 7 for unknown reasons. So, let’s combine these two methods in one to minimize the risk of unwanted scripts running. My tests show the combined approach successfully prevent JavaScript from being executed in all popular browsers. Even in the worst case, at least one of the methods works off. The resultant knockout binding may look like the following:

ko.bindingHandlers.htmlNoScripts = {
    init: function () {
        // Prevent binding on the dynamically-injected HTML
        return { 'controlsDescendantBindings': true };
    },
    update: function (element, valueAccessor, allBindingsAccessor) {
        // First get the latest data that we're bound to
        var value = valueAccessor();
        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.utils.unwrapObservable(value);
        // disable scripts
        var disarmedHtml = valueUnwrapped.replace(/<script(?=(\s|>))/i, '<script type="text/xml" ');        
        // create a wrapping element
        var newSpan = document.createElement('span');
        // safely set internal html of the wrapping element
        newSpan.innerHTML = disarmedHtml;
        // clear the associated node from the previous content
        ko.utils.emptyDomNode(element);
        // add the sanitized html to the DOM
        element.appendChild(newSpan);
    }
};

This htmlNoScripts binding can be used as follows:

<div data-bind="htmlNoScripts: comment" />

Related posts: