Tuesday, August 14, 2012

Part 2 - Consuming custom WCF Data Services (oData) Endpoint (Javascript/jQuery and c#) - Create and Delete

In the first post we have created a service which worked fine for retrieving data. most time we also need delete and create functionality. Here is how it works.

This Article is based on the Part 1 - Consuming custom WCF Data Services. If you just want to download the current version or check out the code, check the codeplex project page.

Preparation:
  • Add JSON2 (via NuGet)
  • Add Styles-Folder
Implementation:
  • Configure DataService to V2
At the moment, the V3 Implementation of Microsoft does not support Create Functionality. You will run into a 415 Unsupported Media Type Exception. More information you can find on Stackoverflow.
The solution for this is quite simple. Just change the value for MaxProtocolVersion in your Service from "V3" to "V2" . 



The difficult part of the Create/Delete Functionality was to create a custom IUpdatable Implementation. Well, this is NOT required for your implementation. You can just replace my implementation with your Entity-Framework Context. 
I want my example independent from database/files. So i have created a static collection of items, controlled by the Context.

  • Replace CarContext.cs code with the following.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
using System.Data.Services;
 
namespace MK.oDataConsuming.Web.Model {
    public class CarContext : IUpdatable {
 
        #region Members
        private static List<Car> _cars;
        #endregion
 
        #region Constructor
        public CarContext() {
            if (_cars == null) {
                _cars = new List<Car>();
            }
        }
        #endregion
 
        #region Properties
        public IQueryable<Car> Cars {
            get {
                return _cars.AsQueryable<Car>();
            }
        }
        #endregion
 
        #region IUpdatable Methods
        public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded) {
            throw new NotImplementedException();
        }
 
        public void ClearChanges() {
            throw new NotImplementedException();
        }
 
        public object CreateResource(string containerName, string fullTypeName) {
 
            Car car = new Car();
 
            int tmpId = 0;
            if (_cars.Count > 0) {
                tmpId = _cars.Max(c => c.Id);
            }
 
            car.Id = ++tmpId;
            return car;
        }
 
        public void DeleteResource(object targetResource) {
            List<Car> carsToDelete = (List<Car>)targetResource;
            Car carToDelete = carsToDelete[0];
            _cars.Remove(carToDelete);
        }
 
        public object GetResource(IQueryable query, string fullTypeName) {
            List<Car> carList = query.Cast<Car>().ToList();
            return carList;
        }
 
        public object GetValue(object targetResource, string propertyName) {
            throw new NotImplementedException();
        }
 
        public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved) {
            throw new NotImplementedException();
        }
 
        public object ResetResource(object resource) {
            throw new NotImplementedException();
        }
 
        public object ResolveResource(object resource) {
            if (resource is Car) {
                _cars.Add((Car)resource);
            }
            return resource;
        }
 
        public void SaveChanges() {
            //We don't have any database/file to store the data in this test.            
        }
 
        public void SetReference(object targetResource, string propertyName, object propertyValue) {
            throw new NotImplementedException();
        }
 
        public void SetValue(object targetResource, string propertyName, object propertyValue) {
            if (propertyName == "Manufacturer") {
                ((Car)targetResource).Manufacturer = (string)propertyValue;
            } else if (propertyName == "ProductionYear") {
                ((Car)targetResource).ProductionYear = (string)propertyValue;
            } else {
                throw new Exception("Property not implemented.");
            }
        }
        #endregion
    }
}



This is not a reference implementation of IUpdatable. At the moment i have not found a good documentation/example. So i have tested a little bit. It works for testing purposes.

  • Add new item (to Styles-Folder) --> Web --> Style Sheet


  • Add css-Code
body 
{
    backgroundnone repeat scroll left top #EEEEEE;
    color#444444;
    font13px Arial,Tahoma,Helvetica,FreeSans,sans-serif;
}
 
div.entry 
{
    background-colorWhite;
    width300px;    
    border1px solid #444444;    
    padding2px;
    margin1px;
}
 
div.entry_delete 
{
    text-alignright;
}
 
div.buttonarea 
{
    text-alignright;
    width300px;    
}
 
div.inputfields 
{
    text-alignright;
    width300px;
}


Only some basic formatting...

  • Update odataaccess.js
The javascript-part is extended with examples for create and delete. I also have added a UI Clear-Function. You can just comment out the first line of the "RetrieveData" Method if you want to see the history of your actions. 
/*
* Retrieve data from service
**/
function RetrieveData() {
    ClearView();
    $.ajax({
        type: "GET",
        async: false,
        contentType: "application/json; charset=utf-8",
        datatype: "json",
        url: "/CarService.svc/Cars",
        success: RetrieveMultipleCallback,
        error: function () {
            alert("Error");
        },
        beforeSend: function (XMLHttpRequest) {
            XMLHttpRequest.setRequestHeader("Accept", "application/json;odata=verbose");
        }
    });
}

/*
* Handles the RetrieveMultiple Response
**/
function RetrieveMultipleCallback(data) {
    var $newDiv;
    $.each(data.d, function (i, item) {
        $newDiv = $("
" + "
" + item.Id + " - " + item.Manufacturer + " - " + item.ProductionYear + "
" + "
" + "
"); $("#listofcars").append($newDiv); }); $newDiv = $("
----
"); $("#listofcars").append($newDiv); } /* * Create data by service **/ function CreateData(id) { var car = {}; var man = $("#man_id").val(); var year = $("#year_id").val(); car.Manufacturer = man; car.ProductionYear = year; var carStringified = JSON.stringify(car); $.ajax({ type: "POST", async: false, data: carStringified, contentType: "application/json", datatype: "json", url: "/CarService.svc/Cars", success: CreateCallback, error: function () { alert("Error"); }, beforeSend: function (XMLHttpRequest) { XMLHttpRequest.setRequestHeader("Accept", "application/json"); } }); } /* * Handles the Create Response **/ function CreateCallback(data) { $("#man_id").val(""); $("#year_id").val(""); RetrieveData(); } /* * Delete data by service **/ function DeleteData(data) { $.ajax({ type: "DELETE", async: false, //data: carStringified, contentType: "application/json", datatype: "json", url: "/CarService.svc/Cars(" + data + ")", success: DeleteCallback, error: function () { alert("Error"); }, beforeSend: function (XMLHttpRequest) { XMLHttpRequest.setRequestHeader("Accept", "application/json"); } }); } /* * Handles the Delete Response **/ function DeleteCallback(data) { RetrieveData(); } /* * Clears the entry-view **/ function ClearView() { $("#listofcars").empty(); }
  • Run and test the Web Project
The List is empty if you start first. So just add some entries by yourself.


Cheers,
Markus

Wednesday, August 8, 2012

Part 1 - Consuming custom WCF Data Services (oData) Endpoint (Javascript/jQuery and c#)

The current version of the WCF Data Services 5.0 is working fine if you are in the .NET domain. Accessing with jQuery can cause some issues.

So here is one example using .NET (c#) and a second one using Javascript (jQuery).

If you want to view the final sourcecode, check it out from codeplex.
http://odataconsuming.codeplex.com/

You also can download the final solution as zip-File.
http://odataconsuming.codeplex.com/downloads/get/467041

You can find Part 2 of this documentation here.

Preparation

Creating Custom WCF Data Service
  • Create new Empty Web Application
  • Add Entity Framework (via NuGet)
  • Add jQuery (via NuGet)
  • Add new Item --> Web --> WCF Data Service (for oData V3)

  • Create entity-class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Services.Common;
 
namespace MK.oDataConsuming.Web {
 
    [DataServiceKey("Id")]
    public class Car {        
        public int Id { getset; }
        public string Manufacturer { getset; }
        public string ProductionYear { getset; }
    }
}
  • Create Context
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
 
namespace MK.oDataConsuming.Web.Model {
    public class CarContext  {
 
        private List<Car> _cars;
 
        public CarContext() {
            _cars = new List<Car>();
            _cars.Add(new Car() { Id = 1, Manufacturer = "BMW", ProductionYear = "1997" });
            _cars.Add(new Car() { Id = 2, Manufacturer = "Mercedes", ProductionYear = "2000" });
            _cars.Add(new Car() { Id = 3, Manufacturer = "Opel", ProductionYear = "2011" });
            _cars.Add(new Car() { Id = 4, Manufacturer = "Ford", ProductionYear = "1999" });
        }
 
        public IQueryable<Car> Cars {
            get {
                return _cars.AsQueryable<Car>();
            }
        }
    }
}
  • Configure access rules
using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Web;
using System.Web;
 
using MK.oDataConsuming.Web.Model;
 
namespace MK.oDataConsuming.Web
{
    public class CarService : DataService<CarContext>
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("Cars"EntitySetRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
        }
    }
}
  • Set .svc as Start Page
  • Test Service



Create HTML Test-Page
  • Add new Item --> Web --> HTML Page
  • Add HTML-Code to file
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>oData Access</title>
    <script src="Scripts/jquery-1.7.2.min.js"></script>
    <script src="Scripts/json2.js"></script>
    <script src="Scripts/odataaccess.js"></script>
</head>
<body>
    <div>
        <input id="RetrieveButton" type="button" value="Retrieve" onclick="RetrieveData()" />
    </div>
    <div id="listofcars">    
    </div>
</body>
</html>
  • Add new Item (to Scripts-Folder) --> Web --> JScript File
  • Add Javascript Code to file
//Retrieve data from service
function RetrieveData() {
    $.ajax({
        type: "GET",
        async: false,
        contentType: "application/json; charset=utf-8",
        datatype: "json",
        url: "/CarService.svc/Cars",
        success: RetrieveMultipleCallback,
        error: function () {
            alert("Error");
        },
        beforeSend: function (XMLHttpRequest) {
            XMLHttpRequest.setRequestHeader("Accept""application/json;odata=verbose");
        }
    });
}
 
//Handles the RetrieveMultiple Response
function RetrieveMultipleCallback(data) {
    $.each(data.d, function (i, item) {
        var $newDiv = $("<div>" + item.Manufacturer + " - "  + item.ProductionYear + "</div>");
        $("#listofcars").append($newDiv);
    });
}

The Most important part ist the "beforeSend" Handler. Here we set the RequestHeader "Accept: application/json;odata=verbose". In older versions of WCF Data Services, this was "Accept: application/json" This does never work!
  • Test the Page


Create Console Application
  • Add new Console Project
  • Add Service Reference

  • Add Code to Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
using MK.oDataConsuming.ConsoleApp.CarServiceReference;
 
namespace MK.oDataConsuming.ConsoleApp {
    class Program {
        static void Main(string[] args) {
 
            //Create Service Contexst
            CarContext context = new CarServiceReference.CarContext(new Uri("http://localhost:1184/CarService.svc"));
            
            //Create query, retrieve and convert to list
            var query = context.Cars.Where(c => c.Manufacturer == "Opel");
            List<Car> resultSet = query.ToList();
 
            //Check results
            if (resultSet != null && resultSet.Count > 0) {
                Car opel = resultSet[0];
                Console.WriteLine("{0}, {1}, {2}", opel.Id, opel.Manufacturer, opel.ProductionYear);
            } else {
                Console.WriteLine("Car not found.");
            }
 
            Console.Read();
        }
    }
}



  • Test the application

Cheers