OpenLightGroup Blog

rss

Blogs from OpenLightGroup.net


Step-By-Step OData 4 / TypeScript / AngularJs / CRUD Sample

image

OData 4 allows you to create back-end services for your web applications with minimal code because it allows the client code to query the entities. This removes the need to create a separate service method for each operation. However, OData 4 is extensible in that it allows you to create special methods if needed. In addition, OData allows you to easy return any entity type you require.

Combined with TypeScript and AngularJs, Odata 4 provides an important component in creating modern SPA (Single Page Applications) quickly and easily.

A Walk-Thru

image

Clicking the All button will show all Tasks and the Complete button will only show those marked complete.

The In-Complete button will only show those Tasks that are not marked complete.

The New Task button will allow you to create a new Task.

image

Clicking on a task name will display a form that will allow you to edit an existing Task.

Create The Application

image

Using Visual Studio 2015, we create a new Project.

image

We create an ASP.NET Web Application.

image

We select the Empty template but select MVC for a core reference to add.

image

The project will be created.

Add Required Packages

image

We have to install the packages the project requires.

Right-click on the project node in the Solution Explorer and select Manage NuGet Packages…

image

If we change the filter to installed we see that we have about 6 packages installed.

There are a lot of packages needed.

Luckily, we can copy the entire list below (at once):

Install-Package angularjs
Install-Package Antlr
Install-Package bootstrap
Install-Package EntityFramework
Install-Package jQuery
Install-Package jQuery.Validation
Install-Package Microsoft.AspNet.Identity.Core
Install-Package Microsoft.AspNet.Identity.EntityFramework
Install-Package Microsoft.AspNet.Mvc
Install-Package Microsoft.AspNet.OData
Install-Package Microsoft.AspNet.Razor
Install-Package Microsoft.AspNet.Web.Optimization
Install-Package Microsoft.AspNet.WebApi
Install-Package Microsoft.AspNet.WebApi.Client
Install-Package Microsoft.AspNet.WebApi.Core
Install-Package Microsoft.AspNet.WebApi.OData
Install-Package Microsoft.AspNet.WebApi.WebHost
Install-Package Microsoft.AspNet.WebPages
Install-Package Microsoft.CodeDom.Providers.DotNetCompilerPlatform
Install-Package Microsoft.Data.Edm
Install-Package Microsoft.Data.OData
Install-Package Microsoft.jQuery.Unobtrusive.Validation
Install-Package Microsoft.Net.Compilers
Install-Package Microsoft.OData.Client
Install-Package Microsoft.OData.Core
Install-Package Microsoft.OData.Edm
Install-Package Microsoft.Spatial
Install-Package Microsoft.Web.Infrastructure
Install-Package Modernizr
Install-Package Newtonsoft.Json
Install-Package System.Spatial
Install-Package WebGrease

image

Then go to the NuGet package Manager Console

image

… click to the right of the PM> symbol and paste the entire list.

image

It will take some time, but it will install all the packages.

(note: you will have to press return when it gets to the last package (WebGrease) to install it).

image

It you open the packages.config file you can confirm what packages are installed.

Add The Database

image

The project will need a database.

Right-click on the App_Data folder and select Add then SQL Server Database.

image

Enter SampleDatabase for name and click the OK button.

image

Double-click on the SampleDatabase.mdf file that will be created in the App_Data folder.

The Server Explorer window should open (if not open it by selecting View then Server Explorer from the Visual Studio toolbar)

In the Server Explorer, under Data Connection / SampleDatabase.mdf, right-click on the Tables folder and select Add New Table.

image

Paste the following script and click the Update button to create the table needed:

CREATE TABLE [dbo].[Tasks] (
    [Id]         INT           IDENTITY (1, 1) NOT NULL,
    [TaskName]   NVARCHAR (50) NOT NULL,
    [IsComplete] BIT           NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC)
);

image

When the Preview Database Updates window opens, select Update Database.

image

If you right-click on the Tables folder and select Refresh

image

… you will see that the Tasks table has now been created.

Add Entity Framework Model

image

We now need to add an Entity Framework model to allow our code to read and write to the database.

Right-Click on the Models folder, then select Add then ADO.NET Entity Data Model.

image

Enter SampleModel for the name and click the OK button.

image

Select EF Designer from database and then click the Next button.

image

It should automatically detect the SampleDatabase, if not select it in the dropdown and click the Next button.

image

Expand the Tables to show the Task table and click the checkbox to select it.

Click the Finish button.

image

Click OK if any Security Warning boxes show up.

image

The Entity Framework model will be created.

Close the window that displays the SampleModel.edmx for now.

Create The MVC Pages

image

We will now create some standard MVC pages so that we can enter some sample data.

First, right-click on the Solution and select Rebuild Solution.

image

Next, right-click on the Controllers folder and select Add then New Scaffolded Item…

image

Select MVC 5 Controller with views, using Entity Framework.

Click the Add button.

image

Select the Task as the Model class and SampleDatabaseEntities as the Data context class.

Name the controller TasksController and click the Add button.

image

The pages will be scaffolded…

image

They will display in the project.

image

Run the project by hitting F5.

image

The web browser will open but you will get an error because the RouteConfig.cs file indicates a default page that does not exist.

image

Navigate to the /Tasks page because that will fire up the TasksController that we just created and the /Tasks/Index.cshtml View page will display.

Click the Create New button to start adding sample Tasks.

image

Add at least five Tasks with various complete statuses and then close the web browser and return to Visual Studio.

Routing

image

We now need to add code that will route calls to the OData 4 endpoint that we will later create.

Add a file to the App_Start folder called WebApiConfig.cs using the following code:

 

using Microsoft.OData.Edm;
using System.Web.Http;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
namespace OData4Sample.App_Start
{
    class WebApiConfig
    {
        #region Register
        public static void Register(HttpConfiguration config)
        {
            // Web API routes 
            config.MapHttpAttributeRoutes();
            config.MapODataServiceRoute(
               routeName: "ODataRoute",
               routePrefix: "odata",
               model: GenerateEntityDataModel());
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
        #endregion
        #region GenerateEntityDataModel
        private static IEdmModel GenerateEntityDataModel()
        {
            ODataModelBuilder builder = new ODataConventionModelBuilder();
            return builder.GetEdmModel();
        }
        #endregion
    }
}

 

Note that while we only need the OData routes, we are adding default routes for WebApi so that you can see how they need to be configured when WebApi and OData are both used at the same time. Basically the OData routes have to be created first.

image

So that the routing code will be triggered (and in the correct order to work properly), open the Global.asax file and change all the code to the following:

 

using OData4Sample.App_Start;
using System.Web.Http;
using System.Web.Routing;
namespace OData4Sample
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
    }
}

 

Create The Default Page

image

We now need to create a default page that will open when the application starts. This is where we will build our AngularJs application.

Right-click on the Controllers folder and select Add then New Scaffolded Item…

Select Controller then MVC 5 Controller – Empty.

Click the Add button.

image

Name the controller, HomeController.

Click the Add button.

image

Right-click on the Home folder (under Views) and select Add then MVC 5 View Page with Layout (Razor).

image

On the Select a Layout Page dialog, select _Layout.cshtml (under Views/Shared).

Click the OK button.

image

Name the view Index.

Click the OK button.

image

Hit F5 to run the application.

The default page will display.

Create The DTO Model

image

In the Models folder, create a file called DTOTask.cs using the following code:

 

using System.ComponentModel.DataAnnotations;
namespace OData4Sample.Models
{
    public class DTOTask
    {
        [Key]
        public int Id { get; set; }
        public string TaskName { get; set; }
        public bool IsComplete { get; set; }
    }
}

 

We call the class DTOTasks rather than Tasks because there is already a class called Tasks that Entity Framework created.

We append DTO in front of this new class because it is a Data Transfer Object. Meaning, this class will be used to transfer data between the various layers of the application.

Create The OData Controller

image

In the Controllers folder, create a file called OData4Controller.cs using the following code:

 

image

 

OData 4 EDM Model Builder

image

We will now update the OData EDM Model.

Open the WebApiConfig.cs file and alter the GenerateEntityDataModel method so that it reads as follows:

 

image

 

This basically configures the application so that the DTOTask entity can be returned as a collection when the OData4Controller is called.

You can get more information on the EDM Model from here:

http://odata.github.io/WebApi/

And here:

http://www.odata.org/blog/how-to-use-web-api-odata-to-build-an-odata-v4-service-without-entity-framework/

 

Add TypeScript

image

First, make sure you have downloaded and installed the latest TypeScript for Visual Studio from:

http://www.typescriptlang.org/#Download

image

Next, add a TypeScript folder and a sub-folder called References.

image

In the references folder, add the following TypeScript definition files:

  • angular.d.ts
  • bootstrap.d.ts
  • jquery.d.ts

You can download the files from DefinitelyTyped.

image

Right-click on the TypeScript directory and select Add then TypeScript File.

image

Name the file oDataApp and use the following code for the file:

 

module ODataApplication {
    export class ODataApp {
        static oDataAppModule: ng.IModule = angular.module('ODataApp', []);
    }
}

This code declares the AngularJs application.

image

Next, make a TypeScript file called oDataController.ts and use the following code:

 

module ODataApplication {
    //#region class Task
    class Task {
        constructor(Id: number, TaskName: string, IsComplete: boolean) {
            this.Id = Id;
            this.TaskName = TaskName;
            this.IsComplete = IsComplete;
        }
        Id: number;
        TaskName: string;
        IsComplete: boolean;
    }
    //#endregion
    export class ODataControllerClass {
        constructor(private $http: ng.IHttpService) {
            this.GetAllTasks();
        }
        //#region Properties
        error: string = "";
        colTasks: Array = new Array();
        selectedTask: Task;
        //#endregion      
        //#region GetAllTasks()
        GetAllTasks() {
            var vm: ODataControllerClass = this;
            vm.selectedTask = null;
            var urlString: string = "/odata/OData4";
            var result: any = vm.$http({
                url: urlString,
                method: "GET"
            });
            result.then(Success, vm.Failure)
            function Success(Tasks: any) {
                vm.colTasks = new Array();
                Tasks.data.value.forEach(task => {
                    vm.colTasks.push(task);
                });
            }
        }
        //#endregion 
        //#region Failure(error: any)
        Failure(error: any) {
            var vm: ODataControllerClass = this;
            if (error.message != undefined) {
                vm.error = error.message;
            }
            else if (error.statusText != undefined) {
                vm.error = error.statusText;
                if (error.data != undefined) {
                    if (error.data.error != undefined) {
                        if (error.data.error.innererror != undefined) {
                            if (error.data.error.innererror.message != undefined) {
                                vm.error = vm.error + ", " + error.data.error.innererror.message;
                            }
                        }
                    }
                }
            }
            else {
                vm.error = "Unknown error";
            }
            alert(vm.error);
        }
        //#endregion
    }
    ODataApp.oDataAppModule
        .controller("oDataController", ["$http", ODataControllerClass]);
}

 

This creates the primary code for the application. For now it just creates the supporting code and contains one primary method (GetAllTasks()) to retrieve all Tasks.

An excellent video by Bill Wagner on how to create AngularJs code using TypeScript is available here: Angular Applications with TypeScript,

Build The project.

image

TypeScript will create .js files for the .ts files (but not the d.ts files). However, they will not be included in the project unless you add them.

Click anywhere in the Server project and select Show All Files.

image

Click on the .js and .js.map files (the .map files are used to allow you to debug the .ts files at run-time), and right-click on them and select Include In Project.

Update The View

image

Update the Views/Home/Index.cshtml file to the following code:

 

@{
    ViewBag.Title = "TypeScript / OData4 / AngularJs Sample";
}
<script src="~/Scripts/angular.js">script>
<script src="~/TypeScript/oDataApp.js">script>
<script src="~/TypeScript/oDataController.js">script>
<style>
    table, th, td {
        border: 1px solid grey;
        border-collapse: collapse;
        padding: 5px;
    }
        table tr:nth-child(odd) {
            background-color: #f1f1f1;
        }
        table tr:nth-child(even) {
            background-color: #ffffff;
        }
style>
<h2>@ViewBag.Titleh2>
<div id="home" class="tab-pane fade in active"
     data-ng-app="ODataApp"
     data-ng-controller="oDataController as vm">
    <br />
    <div style="width:700px">
        <button ng-click="vm.GetAllTasks()">Allbutton> 
        <br /><br />
        <table class="table-responsive">
            <thead>
                <tr>
                    <td>Idtd>
                    <td>TaskNametd>
                    <td>IsCompletetd>
                tr>
            thead>
            <tbody>
                <tr ng-repeat="Task in vm.colTasks track by $index" id="{{Task.Id}}" class="active">
                    <td>{{Task.Id}}td>
                    <td><a href="#" ng-click="vm.selectedTask = Task">{{Task.TaskName}}a> td>
                    <td align="center">
                        <input type="checkbox"
                               ng-model="Task.IsComplete"
                               disabled="disabled">
                    td>
                tr>
            tbody>
        table>
        <br />
    div>
div>

 

Note: While we always code in the .ts files (located in the TypeScript directory), we actually reference the .js files in the markup (above). These files are created by Visual Studio. These are what are used at run-time.

image

When you run the project, you will see the Tasks.

OData Filtering

The primary reason we are using OData for the back-end services is that it is queryable. Meaning, it allows us to compose filtering, that will be executed server-side, from the client.

In the next example, we will demonstrate this by creating a button that will filter Tasks by simply adding this filter:

filter=IsComplete eq true

You can get an introduction to OData filtering at this link: http://www.odata.org/getting-started/basic-tutorial/.

Full documentation is available here:

http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part2-url-conventions.html.

First, we add a button to the View:

image

Next, we add code to the TypeScript / AngularJs controller that will be triggered by the button:

 

        ShowComplete() {
            var vm: ODataControllerClass = this;
            vm.selectedTask = null;
            var urlString: string =
                "/odata/OData4?$filter=IsComplete eq true";
            var result: any = vm.$http({
                url: urlString,
                method: "GET"
            });
            result.then(Success, vm.Failure)
            function Success(Tasks: any) {
                vm.colTasks = new Array();
                Tasks.data.value.forEach(task => {
                    vm.colTasks.push(task);
                });
            }
        }

 

image

When we run the application and click the Complete button, we see that only the completed Tasks are returned.

image

You will want to use the tool such as Fiddler to help you debug issues when developing applications using rest-based services such as OData.

When we run Fiddler, and click the Complete button, we can see the call to the OData endpoint and the JSON payload that is returned.

OData Methods

OData 4 supports Actions and functions. This allows you to call the OData service and easily define parameters and return types.

First, we add the following button to the View:

image

Next, we add code to the TypeScript / AngularJs controller that will be triggered by the button:

 

        ShowInComplete() {
            var vm: ODataControllerClass = this;
            vm.selectedTask = null;
            var urlString: string =
                "/odata/ByStatus(IsComplete=false)";
            var result: any = vm.$http({
                url: urlString,
                method: "POST"
            });
            result.then(Success, vm.Failure)
            function Success(Tasks: any) {
                vm.colTasks = new Array();
                Tasks.data.value.forEach(task => {
                    vm.colTasks.push(task);
                });
            }
        }

 

We add the following code to the EDM Model method in the WebApiConfig.cs file:

 

image

 

Finally, we add the following OData method to the Odata4Controller.cs file:

 

        // odata/ByStatus(paramIsComplete=false)
        [ODataRoute("ByStatus(IsComplete={isComplete})")]
        public IHttpActionResult FilterByStatus([FromODataUri] Boolean IsComplete)
        {
            var result = (from tasks in db.Tasks
                          where tasks.IsComplete == IsComplete
                          select new DTOTask
                          {
                              Id = tasks.Id,
                              TaskName = tasks.TaskName,
                              IsComplete = tasks.IsComplete
                          }).ToList();
            return Ok(result);
        }

 

image

When we run the application and click the In-Complete button, we see that only the uncompleted Tasks are returned.

OData Inserting and Updating

We will now demonstrate how to insert and update data.

First, add a button to allow a user to create a new Task the View:

image

Next, Add a form that will allow an existing Task to be displayed and a new Task to be entered:

 

        <form ng-if="vm.selectedTask">
            <fieldset style="width: 300px; background-color: #bfbfbf;">
                <legend>{{vm.selectedTask.TaskName}}legend>
                <label>
                    <span><strong>Id:strong>span>
                    <span>{{vm.selectedTask.Id}}span>
                    <br />
                    <span><strong>Task Name:strong>span>
                    <input ng-model="vm.selectedTask.TaskName" size="20" />
                    <br />
                    <span><strong>Is Complete:strong>span>
                    <input type="checkbox" ng-model="vm.selectedTask.IsComplete" />
                    <br />
                label>
                <button ng-click="vm.Save()">Savebutton>
                <button ng-click="vm.Delete()">Removebutton>
            fieldset>
        form>

 

Next, we add code to the TypeScript / AngularJs controller that will be triggered by the buttons:

 

        NewTask() {
            var vm: ODataControllerClass = this;
            // Add a new Task - set default values
            vm.selectedTask = new Task(-1, "[New Task]", false);
        }
        Save() {
            var vm: ODataControllerClass = this;
            var urlString: string = "";
            var method: string = "";
            if (vm.selectedTask.Id !== -1) {
                // Perform an Update
                urlString = "/odata/OData4(" + vm.selectedTask.Id + ")";
                method = "PUT";
            }
            else {
                // Perform an Insert
                urlString = "/odata/OData4";
                method = "POST";
            }
            var result: any = vm.$http({
                url: urlString,
                method: method,
                data: vm.selectedTask
            });
            result.then(Success, vm.Failure)
            function Success(Task: any) {
                vm.GetAllTasks();
            };
        }

Note: The method is set to PUT to perform an update and POST to perform an insert.

Finally, we add the following code to the OData service:

 

        public IHttpActionResult Post(DTOTask task)
        {
            var NewTask = new Task();
            NewTask.TaskName = task.TaskName;
            NewTask.IsComplete = task.IsComplete;
            
            db.Tasks.Add(NewTask);
            db.SaveChanges();
            return Created(task);
        }
        public IHttpActionResult Put([FromODataUri] int key, DTOTask task)
        {
            Task ExistingTask = db.Tasks.Find(key);
            if (ExistingTask == null)
            {
                return StatusCode(HttpStatusCode.NotFound);
            }
            ExistingTask.TaskName = task.TaskName;
            ExistingTask.IsComplete = task.IsComplete;
            db.Entry(ExistingTask).State = EntityState.Modified;
            db.SaveChanges();
            return Updated(task);
        }

 

Note: The Post method is for inserting Tasks and the Put method is for updating them.

image

Clicking on an existing task will display the form that will allow that Task to be edited.

Clicking the Save button will update the Task.

image

Clicking the New Task button display the form that will allow that Task to be created.

Clicking the Save button will insert the Task.

OData Deleting

To delete data, add the following method to the TypeScript / AngularJs controller:

 

        Delete() {
            var vm: ODataControllerClass = this;
            var urlString: string = "/odata/OData4(" + vm.selectedTask.Id + ")";
            var result: any = vm.$http({
                url: urlString,
                method: "DELETE"
            });
            result.then(Success)
            function Success(Task: any) {
                vm.GetAllTasks();
            };
        }

Note: The method is set to DELETE.

Finally, add the following code to the OData service:

 

        // DELETE: odata/OData4(5)
        public IHttpActionResult Delete([FromODataUri] int key)
        {
            Task task = db.Tasks.Find(key);
            if (task == null)
            {
                return NotFound();
            }
            db.Tasks.Remove(task);
            db.SaveChanges();
            return StatusCode(HttpStatusCode.NoContent);
        }

 

image

We can delete a Task by first selecting it so that it is displayed in the form, then clicking the Remove button to delete it.

Special Thanks

A special thanks to Richard Waddell @qfeguy for coding assistance in creating this example.

Download

You can download the code at the following link:

OData4Sample.zip





Comments are closed.
Showing 6 Comments
Avatar  [Pingback] 4 months ago

Pingback from scoop.it

Step-By-Step OData 4 / TypeScript / AngularJs /...http://www.scoop.it/t/javascript-for-line-of-business-applications/p/4060501094/2016/03/01/step-by-step-odata-4-typescript-angularjs-crud-sample

Avatar  [Pingback] 4 months ago

Pingback from compksoft.eu

OpenLightGroup Blog | Step-By-Step OData 4 / TypeScript / AngularJs / CRUD Sample | CompkSofthttp://compksoft.eu/?p=34547

Avatar  admin account 4 months ago

@Joshbooker - Thank You!

Avatar  Joshbooker 4 months ago

Here's a quicky on code first migrations w a couple links to tutorials. http://joshuabooker.com/Lists/Posts/Post.aspx?ID=17

Avatar  admin account 4 months ago

@Joshbooker - Thanks for the feedback. I may try code first with migrations after I get more comfortable using it :) Also, If you don't use the DTO object you will get a "Media Formatter not available error" unless you use the EntitySetController which I didn't want to use. I'm not using the patch because since I am using DTO objects I am only sending the fields that I want anyway :)

Avatar  Joshbooker 4 months ago

Nice article Michael. FYI. Put will overwrite the server record while patch will update. I believe. Next time try code first from database with enable-migrations. Avoids the dto and adds easy update and rollback of db Schema changes.