Tag Archives: SharePoint 2010

Client Side Object Model (CSOM) only works in Page Edit Mode

Following my previous post with accessing SharePoint List items using REST API, I’m starting to use Client Side Object Model in the Script Editor Webpart. Then, I found another issue where my CSOM script is only works when the Page in Edit mode. Why would that be ? Why I found two strange things in a week ?

After analyzing and placing breakpoints to the script using developer tools F12, I’ve found that the Out of the Box SharePoint javascript sp.js is not yet loaded when my script executed. Then, I realize my CSOM script really need something to ensure sp.js loaded prior executing my CSOM script => SP.SOD.executeFunc.

Below is my updated code:

<script type="text/javascript">
var ctx;
var listTitle = "My list";
var list;
var items;

// Ensure 'sp.js' is loaded before your code runs.
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', sharePointListReady);

// Create an instance of the current context.
function sharePointListReady() {
 ctx = SP.ClientContext.get_current();
 list = ctx.get_web().get_lists().getByTitle(listTitle);
 var query = new SP.CamlQuery();
 query.ViewXml = "<View><Query><Where><Eq><FieldRef Name='ID'/><Value Type='Counter'>1</Value></Eq></Where></Query></View>";
 items = list.getItems(query);
 ctx.load(items);
 ctx.executeQueryAsync(onRequestSucceeded, onRequestFailed);
}

function onRequestSucceeded() {
 var itemValues = items.getItemAtIndex(0).get_fieldValues();
 alert(itemValues.Title);
}

function onRequestFailed(sender, args) {
 alert('Error: ' + args.get_message());
}
</script>

It works ! great… But the code above returns all items =( it seems like the Caml Query not working. I’ve stuck for quite sometime googling and I found msdn article that use query.set_viewXml(query) to set the Caml Query instead of query.ViewXml. This time is really working.

Below is my final code:

<script type="text/javascript">
var ctx;
var listTitle = "My list";
var list;
var items;

// Ensure 'sp.js' is loaded before your code runs.
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', sharePointListReady);

// Create an instance of the current context.
function sharePointListReady() {
 ctx = SP.ClientContext.get_current();
 list = ctx.get_web().get_lists().getByTitle(listTitle);
 var query = new SP.CamlQuery();
 query.set_viewXml("<View><Query><Where><Eq><FieldRef Name='ID'/><Value Type='Counter'>1</Value></Eq></Where></Query></View>");
 items = list.getItems(query);
 ctx.load(items);
 ctx.executeQueryAsync(onRequestSucceeded, onRequestFailed);
}

function onRequestSucceeded() {
 var itemValues = items.getItemAtIndex(0).get_fieldValues();
 alert(itemValues.Title);
}

function onRequestFailed(sender, args) {
 alert('Error: ' + args.get_message());
}
</script>

References

Advertisements

Retrieve Publishing Image column from SharePoint REST API

Recently, I utilized SharePoint REST API to retrieve list item and one of the column is Publishing Image. I’m half surprise and half confuse why the Publishing Image column is not returned by the REST API. Trying to create a new custom list with Out of the Box Publishing Image site column (Page Image) and I still cannot find the publishing image column from REST API call result.

http://siteurl/_api/web/lists/getbytitle(“listTitle&#8221;)/items?$filter=Active eq 1&$select=Title,PublishingPageImage

Quickly google it and found this post and I’m feeling devastated. Why would you want to make a second call just for the one column.

Below is the same code from the post and Thanks to Vadim Gremyachev

function getJson(endpointUri, success, error)
{
    $.ajax({
       url: endpointUri,
       type: "GET",
       processData: false,
       contentType: "application/json;odata=verbose",
       headers: {
          "Accept": "application/json;odata=verbose"
       },
       success: success,
       error: error
    });
}

function getPublishingPage(webUrl,listName,listItemId,publishingProperties, success, failure)
{
    var itemUri =  webUrl + "/_api/web/lists/getbytitle('" + listName + "')/items(" + listItemId + ")";
    getJson(itemUri,
       function(data){
           var pageItem = data.d;

           var selectProperties = [];
           for(var idx in publishingProperties){
               if(!pageItem.hasOwnProperty(publishingProperties[idx])){
                   selectProperties.push(publishingProperties[idx]);
               }
           }
           if(selectProperties.length > 0) {
              //construct an additional query
              var query = '/FieldValuesAsHtml?$select=' + selectProperties.join(',');
              var endpointUri = pageItem['__metadata'].uri + query;
              getJson(endpointUri,
                 function(data){
                    for(var property in data.d){
                       if(property == "__metadata") continue;
                       pageItem[property] = data.d[property];
                    }
                    success(pageItem);
                 },
                 failure);
           }
           else {
              success(pageItem);
           }
        },
       failure);
}

getPublishingPage(_spPageContextInfo.webAbsoluteUrl,'Pages',3,['PublishingRollupImage','PublishingPageImage'],printPageDetails,logError);

function printPageDetails(pageItem)
{
    console.log('Page Content: ' + pageItem.PublishingPageContent);
    console.log('Page Title: ' + pageItem.Title);
    console.log('Page Rollup Image ' + pageItem.PublishingRollupImage);
}

function logError(error){
    console.log(JSON.stringify(error));
}

References

http://stackoverflow.com/questions/25852997/retrieve-publishing-image-field-with-sharepoint-2013-rest-api-csom

Query Managed Metadata Column in SharePoint List / Library

Query

Following my previous article to Populate Managed Metadata Column, In this article I would like to show you on How to Query Managed Metadata column in SharePoint List / Library.

If you look at the msdn article, the query would look like below:

<Where>
 <In>
  <FieldRef LookupId='TRUE' Name='FieldName' />
  <Values>
   <Value Type='Integer'>14</Value>
   <Value Type='Integer'>15</Value>
  </Values>
 </In>
</Where>

 

The question is where do we get that Values in the query ?

To get those values, I’ve created a SharePoint console application.

static void Main(string[] args)
{
   using(SPSite site = new SPSite("http://yoursiteurl"))
   {
      using (SPWeb web = site.OpenWeb())
      {
         SPList list = web.Lists.TryGetList("MyList");
         TaxonomyField audienceType = (TaxonomyField)site.RootWeb.Fields.GetField("MyManagedColumn");
         TaxonomySession taxonomySession = new TaxonomySession(site);
         TermStore termStore = taxonomySession.TermStores[audienceType.SspId];
         TermSet termSet = termStore.GetTermSet(audienceType.TermSetId);

         TermCollection termColl = termSet.GetAllTerms();
         foreach (Term eachTerm in termColl)
         {
            int[] wssIds = TaxonomyField.GetWssIdsOfTerm(site, termStore.Id, termSet.Id, eachTerm.Id, false, 500);
            if(wssIds.Length > 0)
               Console.WriteLine(string.Format("Term: {0}, WssId: {1}", eachTerm.Name.Trim(), wssIds[0].ToString()));
         }
      }
   }
   Console.ReadLine();
}

 
Those Wss Id are that you need to put as the Value.
 
References

 
 
 
 
 

Configure Information Management Policy programmatically

Following my previous article to set Expiry column using Reusable Workflow, I would also like to Configure Information Management Policy at Content type programmatically on Feature Activated.

What I could do first is to manually configure Information Management Policy at Content type to look like below.

RetentionPolicy1

 

Then, I’ve created simple SharePoint console application to retrieve the Information Management Policy custom data. It looks like below:

using System;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using Microsoft.Office.RecordsManagement.InformationPolicy;

namespace GetRetentionPolicyCustomData
{
    class Program
    {
        static void Main(string[] args)
        {
            using (SPWeb web = new SPSite("http://yourSiteURL").OpenWeb())
            {
                // Get Policy Custom Data
                string contentTypeName = "YourContentTypeName";
                SPContentType contentType = web.ContentTypes.Cast<SPContentType>().Where(cty => cty.Name.Trim().ToLower() == contentTypeName.ToLower()).FirstOrDefault();

                if (contentType != null)
                {
                    Policy policy = Policy.GetPolicy(contentType);
                    if (policy != null)
                    {
                        foreach (PolicyItem eachPolicy in policy.Items)
                        {
                            string customdata = eachPolicy.CustomData;
                            Console.WriteLine("Policy " + eachPolicy.Name + ": " + customdata);
                        }
                    }
                }
            }
        }
    }
}

 

And you will get something like below in Information Management Policy Custom Data variable.

 

<Schedules nextStageId="3">
 <Schedule type="Default">
  <stages>
   <data stageId="1">
    <formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">
     <number>0</number>
     <property>NearExpiry</property>
     <propertyId>15686f6f-8d25-4eba-be79-7792663f4675</propertyId>
     <period>days</period>
    </formula>
    <action type="workflow" id="f5c7ade6-1a7e-4231-bce8-81041b1b7fcd" />
   </data>
   <data stageId="2">
    <formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">
     <number>7</number>
     <property>Expiration</property>
     <propertyId>d2681865-a6d3-4d5d-850b-cd2a1be22b31</propertyId>
     <period>years</period>
    </formula>
    <action type="action" id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.MoveToRecycleBin" />
   </data>
  </stages>
 </Schedule>
</Schedules>

 

Now, Delete the Information Management Policy that you’ve configured manually on the Content Type and let implements it programmatically.
In Site Collection scope feature event receiver:

 

using Microsoft.Office.RecordsManagement.InformationPolicy;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace InformationManagementPolicy
{
    /// <summary>
    /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
    /// </summary>
    /// <remarks>
    /// The GUID attached to this class may be used during packaging and should not be modified.
    /// </remarks>

    [Guid("92cf61e2-0ead-4a9d-b22d-4ff8969e1d05")]
    public class RetentionPolicyEventReceiver : SPFeatureReceiver
    {
        private const string LIST_WORKFLOWTASKS = "Workflow Tasks";
        private const string LIST_WORKFLOWHISTORY = "Workflow History";
        private const string COLUMN_EXPIRATION = "Expiration";
        private const string COLUMN_NEAREXPIRY = "NearExpiry";
        private const string WORKFLOW_SENDNOTIFICATION = "Send Expiry Notification Workflow";

        /// <summary>
        /// Event handler when feature activated
        /// </summary>
        /// <param name="properties"></param>
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPSecurity.RunWithElevatedPrivileges(delegate
                {
                    using (SPWeb web = new SPSite(((SPSite)properties.Feature.Parent).ID).OpenWeb())
                    {
                        string contentTypeName = "YourContentTypeName";
                        SPContentType contentType = web.ContentTypes.Cast<SPContentType>().Where(cty => cty.Name.Trim().ToLower() == contentTypeName.Trim().ToLower()).FirstOrDefault();
                        if (contentType != null)
                        {
                            // Attach Retention Policies to Content Type
                            CreateRetentionPolicyInContentType(web, contentType, COLUMN_EXPIRATION, COLUMN_NEAREXPIRY);
                        }
                    }
                });
            }
            catch (Exception ex)
            {
                // exception "Issue found when CNP.CWE.CDA.Workflows feature activated."
            }
        }

        /// <summary>
        /// Event handler when feature deactivating
        /// </summary>
        /// <param name="properties"></param>
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPSecurity.RunWithElevatedPrivileges(delegate
                {
                    using (SPWeb web = new SPSite(((SPSite)properties.Feature.Parent).ID).OpenWeb())
                    {
                        string contentTypeName = "YourContentTypeName";
                        SPContentType contentType = web.ContentTypes.Cast<SPContentType>().Where(cty => cty.Name.Trim().ToLower() == contentTypeName.Trim().ToLower()).FirstOrDefault();
                        if (contentType != null)
                        {
                            // Delete Retention Policies to Content Type
                            if (Policy.GetPolicy(contentType) != null)
                                Policy.DeletePolicy(contentType);
                        }
                    }
                });
            }
            catch (Exception ex)
            {
                // exception "Issue found when CNP.CWE.CDA.Workflows feature deactivating."
            }
        }

        // Uncomment the method below to handle the event raised after a feature has been installed.
        //public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        //{
        //}

        // Uncomment the method below to handle the event raised before a feature is uninstalled.
        //public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        //{
        //}

        // Uncomment the method below to handle the event raised when a feature is upgrading.
        //public override void FeatureUpgrading(SPFeatureReceiverProperties properties, string upgradeActionName, System.Collections.Generic.IDictionary<string, string> parameters)
        //{
        //}

        #region Private methods

        /// <summary>
        /// Method to attach retention policy to specified content type
        /// </summary>
        /// <param name="web">current web</param>
        /// <param name="contentType">content type</param>
        /// <param name="expirationFieldName">expiration field name</param>
        /// <param name="nearExpiryFieldName">near expiry field name</param>
        private void CreateRetentionPolicyInContentType(SPWeb web, SPContentType contentType, string expirationFieldName, string nearExpiryFieldName)
        {
            try
            {
                SPField expiration = contentType.Fields.Cast<SPField>().Where(fld => fld.InternalName == expirationFieldName).FirstOrDefault();
                SPField nearExpiry = contentType.Fields.Cast<SPField>().Where(fld => fld.InternalName == nearExpiryFieldName).FirstOrDefault();
                SPWorkflowAssociation workflowAssoc = contentType.WorkflowAssociations.Cast<SPWorkflowAssociation>().Where(assoc => assoc.Name == WORKFLOW_SENDNOTIFICATION).FirstOrDefault();

                if (expiration != null && nearExpiry != null && workflowAssoc != null)
                {
                    if (Policy.GetPolicy(contentType) == null)
                        Policy.CreatePolicy(contentType, null);
                    Policy policy = Policy.GetPolicy(contentType);

                    string policyCustomData = GeneratePolicyCustomData(expiration, nearExpiry, workflowAssoc.ParentAssociationId.ToString());
                    policy.Items.Add("Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration", policyCustomData);
                    contentType.Update();
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// Method to generate Policy Custom Data
        /// </summary>
        /// <param name="expiration">expiration column</param>
        /// <param name="nearExpiry">near expiry column</param>
        /// <param name="parentAssociationId">send notification parent workflow association</param>
        /// <returns></returns>
        private string GeneratePolicyCustomData(SPField expiration, SPField nearExpiry, string parentAssociationId)
        {
            StringBuilder sb = new StringBuilder();
            try
            {
                sb.AppendLine("<Schedules nextStageId='3'>");
                sb.AppendLine("<Schedule type='Default'>");
                sb.AppendLine("<stages>");

                // Send Expiry Notification when Today = Near Expiry + 0 days
                sb.AppendLine("<data stageId='1'>");
                sb.AppendLine("<formula id='Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn'>");
                sb.AppendLine("<number>0</number>");
                sb.AppendFormat("<property>{0}</property>", nearExpiry.InternalName);
                sb.AppendFormat("<propertyId>{0}</propertyId>", nearExpiry.Id.ToString());
                sb.AppendLine("<period>days</period>");
                sb.AppendLine("</formula>");
                sb.AppendFormat("<action type='workflow' id='{0}' />", parentAssociationId);
                sb.AppendLine("</data>");

                // Delete the item after Expiration + 7 years
                sb.AppendLine("<data stageId='2'>");
                sb.AppendLine("<formula id='Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn'>");
                sb.AppendLine("<number>7</number>");
                sb.AppendFormat("<property>{0}</property>", expiration.InternalName);
                sb.AppendFormat("<propertyId>{0}</propertyId>", expiration.Id.ToString());
                sb.AppendLine("<period>years</period>");
                sb.AppendLine("</formula>");
                sb.AppendLine("<action type='action' id='Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.MoveToRecycleBin' />");
                sb.AppendLine("</data>");

                sb.AppendLine("</stages>");
                sb.AppendLine("</Schedule>");
                sb.AppendLine("</Schedules>");
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return sb.ToString();
        }
        #endregion
    }
}

Create Custom Http Handler in SharePoint

I would like to share my experience in Creating Custom Http Handler in SharePoint. There are couple of ways to create Http Handler project, but I prefer to use Ashx Handler template from CKSDev Development Tools for SharePoint. It is an easier way to start your Http Handler project and to deploy the solution.

 

CKSDev installation issue

You have to download and install the tool based on your Visual Studio version, so make sure you’re downloading the right version. The other thing that I would like to mention is when I add new item “Ashx handler”, there is a pop up error message look like:

Could not load file or assembly ‘file:///C:\USERS\XXX\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\12.0\EXTENSIONS\WCFZLEFI.TYX\CKS.Dev12.Cmd.Imp.v4.dll’
or one of its dependencies.
This assembly is built by a runtime newer than the currently loaded runtime and cannot be loaded.

Solution to this is to copy all CKS dll into Global Assembly Cache using gacutil command in Visual Studio command prompt. If you don’t face the issue, then probably they have already fixed it.

 

Pre-Requisite

  • CKSDev Tools for SharePoint installed
  • List “MyList” exists in Root Web

 

Implementation 

So without further ado, lets start create one.

1. Create SharePoint Empty Project, called it CustomHttpHandler and Click OK.
CustomHttpHandler1
2. Enter your Site URL and Deploy it as farm solution and Click Finish.
3. Right Click CustomHttpHandler project > Add > New Item, then Select SharePoint 2013 – Ashx handler (CKSDev) template, called it MyAshxHandler and Click Add.
CustomHttpHandler2
4. Now, the solution explorer will look like below and Set the Build Action of MyAshxHandler.ashx to Content.
CustomHttpHandler4
5. Include MyAshxHandler module in the CustomHttpHandler Feature.
CustomHttpHandler4
6. Update MyAshxHandler.ashx.cs code to below:
using System;
using System.Web;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
using System.Linq;

namespace CustomHttpHandler
{
    [Guid("6aa41c89-943a-4242-824b-85c2f433ede3")]
    public partial class MyAshxHandler : IHttpHandler
    {
        private const string ParameterName = "Title";
        private const string ColumnName = "Title";
        #region IHttpHandler Members

        /// <summary>
        /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
        /// </summary>
        /// <value></value>
        /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.
        /// </returns>
        public bool IsReusable
        {
            get { return false; }
        }

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
        public void ProcessRequest(HttpContext context)
        {
            try
            {
                context.Response.Clear();

                // Get Title parameter
                string title = GetParameters(ParameterName);
                if(title != string.Empty)
                {
                    Guid siteId = SPContext.Current.Site.ID;

                    // Elevate permission for Anonymous
                    SPSecurity.RunWithElevatedPrivileges(delegate
                    {
                        using (SPWeb web = new SPSite(siteId).OpenWeb())
                        {
                            // Get List
                            SPList myList = web.Lists.Cast<SPList>().Where(lst => lst.Title == "MyList").FirstOrDefault();
                            if (myList == null)
                                throw new Exception("Bad Request, My list is NOT found ...");

                            // Get List item
                            SPListItem myItem = myList.Items.Cast<SPListItem>().Where(strslt => strslt[ColumnName].ToString() == title).FirstOrDefault();

                            // Add / Update Item
                            web.AllowUnsafeUpdates = true;

                            // If new item
                            if (myItem == null)
                                myItem = myList.Items.Add();

                            myItem[ColumnName] = title;
                            myItem.Update();

                            web.AllowUnsafeUpdates = false;

                            // Set the response
                            context.Response.StatusCode = 200;
                            context.Response.StatusDescription = "Successful";
                            context.Response.ContentType = "text/plain";
                            context.Response.Write(String.Format("Item with GUID:{0} and Title:{1} has been created / updated", myItem.ID.ToString(), myItem[ColumnName].ToString()));
                        }
                    });
                }
            }
            catch(Exception ex)
            {
                context.Response.Clear();
                context.Response.StatusCode = 400;
                context.Response.StatusDescription = ex.Message;
            }
            context.Response.End();
        }
        #endregion

        #region private methods
        private string GetParameters(string queryParam)
        {
            try
            {
                return HttpContext.Current.Request.QueryString[queryParam];
            }
            catch (Exception)
            {
                return string.Empty;
            }
        }
        #endregion
    }
}
7. Build and Deploy the solution.

 

Results

CustomHttpHandler5

CustomHttpHandler6

 

References

SharePoint 2013 New Delegate Controls

Using SharePoint delegate control, a developer can customize the SharePoint site controls without editing or even touching the master page.
Most of us already knows existing delegate controls from previous versions of SharePoint.

  • AdditionalPageHead
  • GlobalNavigation
  • GlobalSiteLink0
  • GlobalSiteLink2
  • GlobalSiteLink3
  • PublishingConsole
  • PageHeader
  • TopNavigationDataSource
  • TreeViewAndDataSource
  • PageFooter
  • QuickLaunchDataSource
  • SmallSearchInputBox

You could find more details on those delegate controls on Daniel’s blog. Developers could also create new delegate control of their own and include it in their custom master page like I found here.

Now with SharePoint 2013, they introduce new Delegate Controls below.

SuiteBarBrandingDelegate
SuiteBarBranding
SuiteLinksDelegate
SuiteLinksDelegate
PromotedActions
PromotedActions

I found excellent article with example that’s really helpful and used mostly with my client project in Tobias Zimmergren blog.

Retrieve List Items Using Knockout.js with SharePoint REST API

Recently, I’m trying to use Client side object model or java script whenever possible. I’m sure there are multiple ways to retrieve List items, but in this scenario I will use SharePoint REST API and utilizing Knockout view model to display the list of items.

Prerequisite

  • Includes jquery.min.js to Style Library
  • Includes knockout.js to Style Library
  • Includes ko-sp.js to Style Library
  • Create Majors list
    ko1
  • Create Courses list
    ko2
  • Create Students list
    ko3
Solution

  • Edit a Page and Insert Content Editor webpart
    ko4
  • Edit Content Editor Source and insert below code:
    <script src="/Style%20Library/jquery-1.11.0.min.js"></script>
    <script src="/Style%20Library/knockout-3.1.0.js"></script>
    <script src="/Style%20Library/ko.sp-1.0.min.js"></script>
    <div id="restStudentDiv">
    	<table width="900px">
    		<thead>
    			<tr>
    				<th>SID</th>
    				<th>Name</th>
    				<th>Major</th>
    				<th>Courses</th>
    				<th>Status</th>
    			</tr>
    		</thead>
    		<tbody data-bind="template: { name: 'templateStudent', foreach: Items }" />
    	</table>
    </div>
    <script type="text/html" id="templateStudent">
    	<tr>
    		<td data-bind="text:SID"></td>
    		<td data-bind="text:Title"></td>
    		<td data-bind="spLookup:Major,defaultValue:'NA'"></td>
    		<td data-bind="spLookup:Courses,multi:true,defaultValue:'NA',dataFormat:'/'"></td>
    		<td data-bind="spChoice:Status"></td>
    	</tr>
    </script>
    <script type="text/javascript">
    	function StudentModal() {
    		var self = this;
    		self.Items = ko.observableArray([]);
    		$.getJSON(_spPageContextInfo.webAbsoluteUrl + "/_vti_bin/listdata.svc/Students?$expand=Major,Courses",
    		function (data) {
    			if (data.d.results) {
    				self.Items(ko.toJS(data.d.results));
    			}
    		}
    		);
    	}
    	ko.applyBindings(new StudentModal(), restStudentDiv);
    </script>
    
Result
ko5
%d bloggers like this: