Mar 27

Written by: Michael Washington
3/27/2016 10:59 AM  RssIcon

Migrating From LightSwitch

If you are ready to move from Visual Studio LightSwitch to the latest web technologies, the example in this article may provide a useful example. This application recreates the Online Ordering System (An End-To-End LightSwitch Example) that has been used on this site for years.

The key technologies that will be covered are:

  • AngularJs- The most popular JavaScript framework for building modern web applications.
  • OData 4 – This is used to provide server-side data endpoints that are called by the AngularJs code. This has more features than OData 3 that is in LightSwitch. OData 4 provides custom methods. See the CurrentUser and OrderDetailsForOrder methods in this example.
  • TypeScript – This allows you to write JavaScript code that has strong typing and compile-time checking and validation.
  • Bootstrap – Like JQuery mobile in LightSwitch, this allows you to create a UI that scales from desktop to mobile with minimal coding.

Online Example

image

You can try out the live online application at: https://endtoendangular1.lightswitchhelpwebsite.com/ (you will have to create a new account and log in to use it).

This application recreates the Online Ordering System (An End-To-End LightSwitch Example) that has been used on the LightSwitchHelpWebsite.com for years.

The sample application has the following features:

  • Products
    • Add Products
    • Edit Products 
    • Delete Products
  • Orders
    • Add Orders
    • Edit Orders
    • Add Order Details
    • Edit Order Details
    • Delete Order Details
    • Delete Orders
  • Business Rules
    • Allow the current user to only see their own Orders
    • Administrator(s) can see and edit all Orders

 

Walk-Thru

image

Orders are displayed in a pageable grid.

They can be edited and deleted.

Use the Add New Order button to create a new Order.

image

When adding or editing an Order, an Order date can be set, and the details added, by clicking the Add Order Detail button.

Order Details can also be edited or deleted.

 

image

When adding or editing an Order Detail, the Product and Quantity is selected using the Edit Order Detail dialog.

image

The Products are edited using a standard MVC page that is only accessible to Administrators.

Building The Application

 

image

Using Visual Studio 2015, we start with the application created in Creating User and Roles Administration Pages for an MVC5 Application.

This provides the user registration, and the roles and management the application requires, as well as creating the database and the database connection.

image
We then add Code First Entity Framework Migrations (you can get more information on how this works at: http://www.entityframeworktutorial.net/code-first/automated-migration-in-code-first.aspx).

Note: Migrations can be a bit unwieldy to manage so I normally use Entity Framework Designer as shown in the example here: http://openlightgroup.com/Blog/TabId/58/PostId/188/OData4Sample.aspx. This article is a MUST read: Entity Framework Code First Migrations).

  image

For this project, because we do have migrations enabled, we can create a DAL (Data Access Layer) class:

 

    public class EndToEndDAL : DbContext
    {
        public EndToEndDAL()
            : base("name=DefaultConnection")
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<EndToEndDAL, 
                EndToEnd_Test.Migrations.Configuration>("DefaultConnection"));
        }
        // Add a DbSet for each entity type that you want to include in your model. For more information 
        // on configuring and using a Code First model, see http://go.microsoft.com/fwlink/?LinkId=390109.
        public virtual DbSet<Product> Products { get; set; }
        public virtual DbSet<Order> Orders { get; set; }
        public virtual DbSet<OrderDetail> OrderDetails { get; set; }
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }
    }
    public class Product
    {
        public int Id { get; set; }
        [Required]
        [MaxLength(255)]
        public string ProductName { get; set; }
        [Column(TypeName = "Money")]
        public decimal ProductPrice { get; set; }
    }
    public class Order
    {
        public int Id { get; set; }
        [Required]
        [MaxLength(255)]
        public string UserName { get; set; }
        [Required]
        [Column(TypeName = "Date")]
        public DateTime OrderDate { get; set; }
        // Navigation property 
        public virtual ICollection<OrderDetail> OrderDetails { get; set; }
    }
    public class OrderDetail
    {
        public int Id { get; set; }
        [Required]
        public int Quantity { get; set; }
        // Foreign key 
        [Required]
        public int OrderId { get; set; }
        // Navigation property 
        [Required]
        public virtual Product Product { get; set; }
    }

 

When the project is run and hits a piece of code that connects to the database, the migrations run, and the database schema is automatically created (or if it needs to be, updated):

image

The Products Administration

image

The administration for the Products table is composed of pure MVC pages. This is how they were created.

First, we right-clicked on the Controllers folder and selected Add then New Scaffolded item…

image

Next, we selected MVC 5 Controller with views, using Entity Framework.

image

Finally, we select Product as the Model, EndToEndDAL as the Data context, and name the Controller ProductsController and press Add.

image

The Products View and Controller are automatically created.

image

We then decorate the methods in the Controller with [Authorize(Roles = "Administrator")] so that only Administrators can alter the table (users will read from the table through the OData methods that we will add later).

image28

We then open the _Layout.cshtml view and add an Administrator's only link to the Products view. 

More Set-up

We also need to install AngularJs and OData 4.

image

We go to the NuGet package Manager Console

image

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

Install-Package Angularjs
Install-Package Microsoft.OData.Client
Install-Package Microsoft.OData.Core
Install-Package Microsoft.OData.Edm
Install-Package Microsoft.Spatial
Install-Package Microsoft.AspNet.OData
Install-Package Microsoft.AspNet.WebApi.WebHost

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

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

image

We update the WebApiConfig.cs file to the following to enable OData:

 

using EndToEnd_Test.Models;
using Microsoft.OData.Edm;
using System.Web.Http;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
namespace EndToEnd_Test
{
    class WebApiConfig
    {
        #region Register
        public static void Register(HttpConfiguration config)
        {
            // OData routes
            // These must be configured before the WebAPI routes 
            config.MapHttpAttributeRoutes();
            config.MapODataServiceRoute(
               routeName: "ODataRoute",
               routePrefix: "odata",
               model: GenerateEntityDataModel());
            // Web API routes 
            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();
            // ** The OData controllers will be configured here
            return builder.GetEdmModel();
        }
        #endregion
    }
}

 

image

We also update the Global.asax file to ensure that WebApiConfig is the first item in the Application_Start() method.

Basically OData has to be configured first or it wont work.

Create DTO Classes

image

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

 

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EndToEnd_Test.Models
{
    public class DTOProduct
    {
        [Key]
        public int Id { get; set; }
        public string ProductName { get; set; }
        public string ProductPrice { get; set; }
    }
    public class DTOOrder
    {
        [Key]
        public int Id { get; set; }
        public string UserName { get; set; }
        public string OrderDate { get; set; }
        public ICollection<DTOOrderDetail> colOrderDetails { get; set; }
    }
    public class DTOOrderDetail
    {
        [Key]
        public int Id { get; set; }
        public int Quantity { get; set; }
        public int ProductId { get; set; }
    }
}

 

We prepend DTO in front of the class name to indicate that each is a Data Transfer Object. Meaning, these classes will be used to transfer data between the AngularJs TypeScript code and the server-side OData methods.

You will always want to specify a [Key] for each class and always populate it with a unique value when using it.

 

image

Make sure we have downloaded and installed the latest TypeScript for Visual Studio from:

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

image

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

In the References folder, we add the following TypeScript definition files:

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

(you can download the files from DefinitelyTyped)

These files are required to allow TypeScript to provide compile-time validation of our custom code that calls methods from these libraries.

image

Now, 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', ['ngAnimate', 'ui.bootstrap']);
    }
}

This code declares the AngularJs application, called ODataApp, and injects ngAnimate and ui-bootstrap dependencies into the application.

image

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

 

module ODataApplication {
    //#region classes
    // These classes allow us to use strong typing
    // in our code. We want to avoid typing any 
    // variable or property as 'any'
    class Product {
        Id: number;
        ProductName: string;
        ProductPrice: string;
    }
    class Order {
        Id: number;
        UserName: string;
        OrderDate: string;
        colOrderDetails: Array<OrderDetail>;
    }
    class OrderDetail {
        Id: number;
        Quantity: number;
        ProductId: number;
        Product: Product;
    }
    class ProductArray {
        value: Array<Product>
    }
    class GetProductArrayResults {
        data: ProductArray;
    }
    class OrderArray {
        value: Array<Order>
    }
    class GetOrderArrayResults {
        data: OrderArray;
    }
    class OrderDetailsArray {
        value: Array<OrderDetail>
    }
    class GetOrderDetailsArrayResults {
        data: OrderDetailsArray;
    }
    class GetOrderResults {
        data: Order;
    }
    //#endregion
    var vm: ODataControllerClass;
    export class ODataControllerClass {
        constructor(private $http: ng.IHttpService) {
            // Set a global 'vm' to 'this' so that we have access
            // to all properties and methods in the view model
            // in all the methods.
            vm = this;
            // Call the method to make a server-side call to 
            // get the name of the currrently loogged in user.
            this.GetCurrentUser();
            // Call the method to make a server-side call to 
            // get the Products. This will fill a collection
            // that the Products dropdown on the Edit Order Details 
            // page is bound to.
            this.GetAllProducts();
            // Call the method to make a server-side call to 
            // get the Orders. This will fill a collection
            // that the Orders is bound to.
            this.GetAllOrders();
        }
        //#region Properties        
        error: string = "";
        // We need to base URL for the Odata service calls
        baseUrl: string = $("#linkRoot").attr("href");
        currentUser: string = "";
        colProducts: Array<Product> = new Array<Product>();
        colOrders: Array<Order> = new Array<Order>();
        selectedOrder: Order = null;
        selectedProduct: Product = null;
        selectedOrderDetail: OrderDetail = null;
        totalOrderCount: number = 0;
        currentOrderPage: number = 1;
        //#endregion      
        //#region Failure(error: any)
        Failure(error: any) {
            // Process any errors
            // This method is called whenever there is a failure
            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";
            }
            // Display the error
            alert(vm.error);
        }
        //#endregion
        //#region copyObject<T>(object: T): T
        copyObject<T>(object: T): T {
            var objectCopy = <T>{};
            for (var key in object) {
                if (object.hasOwnProperty(key)) {
                    objectCopy[key] = object[key];
                }
            }
            return objectCopy;
        }
        //#endregion
    }
    // This is actually the first thing to run in this module.
    // This is placed at the end because the 'ODataControllerClass' (above),
    // that is being registered, has to be defined first. 
    ODataApp.oDataAppModule
        .controller("oDataController", ["$http", ODataControllerClass]);
}

 

This file will contain the primary code for the application. For now it just creates the supporting code.

Notice that we define classes for Product, Order and OrderDetail. We will fill these classes with data when we make calls to the OData methods that will transmit that data using the DTO classes that we created earlier.

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

Also, I created a tutorial here: Step-By-Step OData 4 / TypeScript / AngularJs / CRUD Sample.

 

Set-Up The Page

image

All the remaining markup will be contained in the Index.cshtml file that is located in the Home folder under Views.

image

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 contain pure JavaScript and are created by the TypeScript compiler in Visual Studio each time the application is compiled. The .js files are what are used at run-time.

After registering the oDataApp and oDataController files we enable them in the application in the outermost DIV of the page markup using this code:

 

    <div id="home" class="tab-pane fade in active"
         data-ng-app="ODataApp"
         data-ng-controller="oDataController as vm">

 

New Order

image

A good place to start to examine the application is the Add New Order button.

The button is declared using the following code:

 

        <button id="AddNewOrderButton"
                type="button"
                class="btn btn-primary btn-sm"
                data-dismiss="modal"
                ng-click="vm.AddNewOrder()">
            Add New Order
        </button>   

 

The EditOrderModal popup will open when the button is clicked because we also have the following JavaScript on the page:

 

    <!-- JavaScript -->
    <script>
    $(document).ready(function () {
        $('#AddNewOrderButton').on('click', function () {
            $('#EditOrderModal').modal('show');
        });
    });
    </script>

 

The ng-click property in the button markup calls the following AddNewOrder() method in the AngularJs TypeScript Controller:

 

        AddNewOrder() {
            // Create an empty Order
            vm.selectedOrder = new Order();
            // Set Id to 0 so the server side code will 
            // know this is an insert
            vm.selectedOrder.Id = 0;
            // Call the CurrentDate() method to get the current date
            vm.selectedOrder.OrderDate = vm.CurrentDate();
            // Create an empty colOrderDetails so that 
            // Order Detail records can be appended to it
            vm.selectedOrder.colOrderDetails = new Array<OrderDetail>();     
            // Set the current user. This value was set by the 
            // call to this.GetCurrentUser() in the constructor 
            // of the Angular controller class    
            vm.selectedOrder.UserName = vm.currentUser;
        }

 

This method consumes the name of the Current User, that is retrieved from a call (that we place in the module constructor), to the following CurrentUser() method:

 

        GetCurrentUser() {
            // Get the name of the current user
            // Construct the path to the OData end point
            var urlString: string = vm.baseUrl + "odata/CurrentUser";
            var result: any = vm.$http({
                url: urlString,
                method: "POST"
            });
            // Process the result of the OData call
            // If successful, the Success method will be called
            // If not successful the Failure method will be called 
            result.then(Success, vm.Failure)
            function Success(CurrentUser: any) {
                // Set the name of the current user
                vm.currentUser = CurrentUser.data.value;
            }
        }

 

This method calls the OData end-point called CurrentUser.

image

To implement it, we create a controller file called OData4Controller.cs using the following code:

 

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web.Http;
using System.Web.OData;
using System.Web.OData.Routing;
using EndToEnd_Test.Models;
namespace EndToEnd_Test.Controllers
{
    public class OData4Controller : ODataController
    {
        private EndToEndDAL db = new EndToEndDAL();
        #region public IHttpActionResult CurrentUser()
        // odata/CurrentUser()
        [ODataRoute("CurrentUser()")]
        public IHttpActionResult CurrentUser()
        {
            return Ok(this.User.Identity.Name);
        }
        #endregion
        // Utility
        #region protected override void Dispose(bool disposing)
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
        #endregion
    }
}

 

To enable this OData method, we add the following code to the GenerateEntityDataModel() method in the WebApiConfig.cs file:

 

            // CurrentUser function returns string
            var CurrentUserFunction = builder.Function("CurrentUser");
            CurrentUserFunction.Returns<string>();

 

The following diagram illustrates the connection to the various parts:

image

Edit Order Detail

 

image

The markup for the Add Order Detail button is as follows:

 

    <button type="button" 
            id="EditOrderDetailButton"
            title="Edit"
            class="btn btn-primary btn-xs glyphicon glyphicon-edit"
            onclick="$('#EditOrderDetail').modal('show');"
            ng-click="vm.selectedOrderDetail = OrderDetail">
    </button>

 

When you click on the Add Order Detail button, it sets the current OrderDetail as the selectedOrderDetail and the Edit Order Detail popup opens:

 

image

The Edit Order Detail popup contains a dropdown of the available Products (it also needs to indicate the selected Product if the popup is opened for an existing record).

The markup for the dropdown is as follows:

 

        <select name="ProductDropdown" 
                class="form-control" 
                ng-model="vm.selectedOrderDetail.Product" 
                ng-options="Product.ProductName for Product in vm.colProducts">
        </select>

 

Note: the ng-model for the dropdown binds the dropdown to the Product property of the selectedOrderDetail (that was set when the Add Order Detail button was clicked).

The ng-options for the dropdown, indicates that the ProductName is to be displayed from the collection contained in the colProducts property (that is composed of an Array of Products) that is filled by the following code in the controller:

 

        GetAllProducts() {
            // Construct the path to the OData end point
            var urlString: string = vm.baseUrl + "odata/ODataProducts";
            // Call the OData end point
            var result: ng.IPromise<GetProductArrayResults> = vm.$http({
                url: urlString,
                method: "GET"
            });
            // Process the result of the OData call
            // If successful, the Success method will be called
            // If not successful the Failure method will be called 
            result.then(Success, vm.Failure)
            // Handle a successful call
            function Success(results: GetProductArrayResults) {
                // Fill the Products collection
                vm.colProducts = results.data.value;
            }
        }

 

This method calls the OData end-point called ODataProducts.

Note: That while the AngularJs code just makes a call to ODataProducts, the server-side OData method is actually called GetODataProducts. The reason is that when calling an OData queryable method (one that implements IQueryable) you use the word Get in front of the method name.

To enable it, we create a controller file called ODataProductsController.cs using the following code:

 

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web.Http;
using System.Web.OData;
using System.Web.OData.Routing;
using EndToEnd_Test.Models;
namespace EndToEnd_Test.Controllers
{
    public class ODataProductsController : ODataController
    {
        private EndToEndDAL db = new EndToEndDAL();
        #region public IQueryable<DTOProduct> GetODataProducts()
        // GET: odata/ODataProducts
        [Authorize]
        [EnableQuery(PageSize = 100)]
        public IQueryable<DTOProduct> GetODataProducts()
        {
            var result = (from product in db.Products
                          select new DTOProduct
                          {
                              Id = product.Id,
                              ProductName = product.ProductName,
                              ProductPrice = product.ProductPrice.ToString()
                          });
            return result.AsQueryable();
        } 
        #endregion
        // Utility
        #region protected override void Dispose(bool disposing)
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
        #endregion
    }
}

 

Note: We decorate the method by setting the PageSize property of EnableQuery. This is important to not allow a malicious user to manually pass a query that could bring down the database by asking for too many records at once.

To enable this OData method, we add the following code to the GenerateEntityDataModel() method in the WebApiConfig.cs file:

 

            // Register ODataProducts
            builder.EntitySet<DTOProduct>("ODataProducts");

 

 

Save Order Detail

 

image

The following markup is used for the Save button:

 

    <button type="button" 
            class="btn btn-success btn-sm" 
            ng-click="vm.UpdateOrderDetail()">Save</button>

 

This calls the UpdateOrderDetail() method that adds or updates the selection in the colOrderDetails collection of the selectedOrder:

 

        UpdateOrderDetail() {
            // Validate that we have a valid selectedOrderDetail.
            // We must have a product and the Quantity must be filled in
            // and Quantity must be a valid number
            if (vm.selectedOrderDetail.Product == null
                || isNaN(vm.selectedOrderDetail.Quantity)
                || vm.selectedOrderDetail.Quantity.toString() == "") {
                // Show an error message
                alert("Product and Quantity are required");
            } else {  
                // If Id = 0 this is an insert
                if (vm.selectedOrderDetail.Id === 0) {
                    // Change Id to -1 so we will know it not an insert  
                    // next time the record comes to this method        
                    vm.selectedOrderDetail.Id = -1;
                    // Add the selectedOrderDetail to the colOrderDetails 
                    // collection of the Order.
                    vm.selectedOrder.colOrderDetails.push(vm.selectedOrderDetail);
                } 
                // Close the OrderDetail dialog
                $('#EditOrderDetail').modal('hide')
            }
        }

 

image

The binding in Angular will causes the OrderDetail item to show in the table on the Edit Order popup due to the binding markup that is used.

The following shows the complete markup for the table:

 

<table class="table table-hover
        table-bordered table-striped table-responsive table-condensed">
        <thead>
            <tr>
                <th align="center">Edit</th>
                <th>Product</th>
                <th>Quantity</th>
            </tr>
        </thead>
        <tbody>
            <tr ng-repeat="OrderDetail in vm.selectedOrder.colOrderDetails track by $index" 
                id="{{OrderDetail.Id}}"
                class="active">
                <td align="center">
                    <button type="button" 
                            id="EditOrderDetailButton"
                            title="Edit"
                            class="btn btn-primary btn-xs glyphicon glyphicon-edit"
                            onclick="$('#EditOrderDetail').modal('show');"
                            ng-click="vm.selectedOrderDetail = OrderDetail">
                    </button>
                    <button type="button" 
                            id="DeleteOrderButton"
                            title="Delete"
                            class="btn btn-danger btn-xs glyphicon glyphicon-trash"
                            ng-click="vm.selectedOrderDetail = OrderDetail ; vm.DeleteOrderDetail()">
                    </button>
                </td>
                <td><span>{{OrderDetail.Product.ProductName}}</span></td>
                <td align="center">{{OrderDetail.Quantity}}</td>
            </tr>
        </tbody>
    </table>

 

The ng-repeat is the key piece of code that loops through items in the colOrderDetails collection of the selectedOrder.

Save Order

image

An Order consists of the Order entity and possibly multiple OrderDetail entities that each have an associated Product.

The Save button on the Edit Order popup is interesting in that it saves them all in a single call.

The following is the markup for the Save button:

 

    <button type="button" 
            class="btn btn-success btn-sm"
            ng-click="vm.SaveOrder()">Save</button>

 

This calls the SaveOrder() method that will perform a PUT if it is update and a POST if it is an insert:

 

        SaveOrder() {
            // Construct the path to the OData end point
            var urlString: string = "";
            var method: string = "";
            if (vm.selectedOrder.Id !== 0) {
                // Perform an Update
                urlString = vm.baseUrl + "odata/ODataOrders(" + vm.selectedOrder.Id + ")";
                method = "PUT";
            } else {
                // Perform an Insert
                urlString = vm.baseUrl + "odata/ODataOrders";
                method = "POST";
            }
            // Create an Order to pass to the OData method
            var objOrder: Order = new Order();
            objOrder.Id = vm.selectedOrder.Id;
            objOrder.OrderDate = vm.selectedOrder.OrderDate;
            objOrder.UserName = vm.selectedOrder.UserName;
            objOrder.colOrderDetails = new Array<OrderDetail>();
            vm.selectedOrder.colOrderDetails.forEach(OrderDetail => {
                // Make a copy of the OrderDetail record so that the 
                // Actual record is not affected when we wipe out the 
                // Product property
                var objOrderDetail: OrderDetail = vm.copyObject(OrderDetail);
                // Set the ProductId
                objOrderDetail.ProductId = OrderDetail.Product.Id;
                // Clear Product out so Odata method wont fail
                // OData only handles one level of nesting not 
                // the two levels that passing the Product would require.
                objOrderDetail.Product = undefined;
                objOrder.colOrderDetails.push(objOrderDetail);
            });
            var result: ng.IPromise<GetOrderResults> = vm.$http({
                url: urlString,
                method: method,
                // Pass the Order (and its OrderDetails) 
                // to the OData method
                data: objOrder
            });
            // Process the result of the OData call
            // If successful, the Success method will be called
            // If not successful the Failure method will be called 
            result.then(Success, vm.Failure)
            function Success(results: GetOrderResults) {
                // Set the selectedOrder to the Order that 
                // was pased back from the OData method
                // If this is an insert
                // No data is passed if this is an update
                if (results.data !== undefined) {
                    vm.selectedOrder = results.data;
                }
                // Refresh the Orders
                vm.GetAllOrders();
                // Close the Order dialog
                $('#EditOrderModal').modal('hide')
            }
        }

 

image

We create a new ODataOrdersController.cs file with the following markup:

 

        #region public IHttpActionResult Post(DTOOrder dtoOrder)
        // POST: odata/ODataOrders
        [Authorize]
        public IHttpActionResult Post(DTOOrder dtoOrder)
        {
            // Create a new Order
            var NewOrder = new Order();
            NewOrder.OrderDate = Convert.ToDateTime(dtoOrder.OrderDate);
            NewOrder.UserName = this.User.Identity.Name;
            NewOrder.OrderDetails = new List<OrderDetail>();
            // Add the Order Details
            // Loop through all the Order Details passed
            foreach (var orderDetail in dtoOrder.colOrderDetails)
            {
                // Create a new Order Detail
                var NewOrderDetail = new OrderDetail();
                // Set the Quanity
                NewOrderDetail.Quantity = orderDetail.Quantity;
                // Get the Product from the ProductId
                NewOrderDetail.Product =
                    db.Products.Where(x => x.Id == orderDetail.ProductId)
                    .FirstOrDefault();
                // Add the Product to the Order
                NewOrder.OrderDetails.Add(NewOrderDetail);
            }
            // Save the Order
            db.Orders.Add(NewOrder);
            db.SaveChanges();
            // Populate the ID that was created and pass it back
            dtoOrder.Id = NewOrder.Id;
            return Created(dtoOrder);
        }
        #endregion
        #region public IHttpActionResult Put([FromODataUri] int key, DTOOrder dtoOrder)
        // PUT: odata/ODataOrders(1)
        [Authorize]
        public IHttpActionResult Put([FromODataUri] int key, DTOOrder dtoOrder)
        {
            Order ExistingOrder = db.Orders.Find(key);
            if (ExistingOrder == null)
            {
                return StatusCode(HttpStatusCode.NotFound);
            }
            // Only Administrators or the orginal user
            // can edit the Order
            if (!this.User.IsInRole("Administrator"))
            {
                if (ExistingOrder.UserName != this.User.Identity.Name)
                {
                    throw new Exception(
                        String.Format("Record belongs to {0} and the curent user is {1}",
                        ExistingOrder.UserName,
                        this.User.Identity.Name));
                }
            }
            // Update the Order Date
            ExistingOrder.OrderDate = Convert.ToDateTime(dtoOrder.OrderDate);
            // Delete all existing Order Details
            db.Database.ExecuteSqlCommand("Delete from OrderDetails where OrderId = @OrderId", 
                new SqlParameter("@OrderId", ExistingOrder.Id));
            db.SaveChanges();
            // Add the Order Details
            // Loop through all the Order Details passed
            foreach (var orderDetail in dtoOrder.colOrderDetails)
            {
                // Create a new Order Detail
                var NewOrderDetail = new OrderDetail();
                // Set the Foreign key to Order
                NewOrderDetail.OrderId = ExistingOrder.Id;
                // Set the Quanity
                NewOrderDetail.Quantity = orderDetail.Quantity;
                // Get the Product from the ProductId
                NewOrderDetail.Product =
                    db.Products.Where(x => x.Id == orderDetail.ProductId)
                    .FirstOrDefault();
                // Add the Product to the Order
                ExistingOrder.OrderDetails.Add(NewOrderDetail);
            }
            // Save changes
            db.Entry(ExistingOrder).State = EntityState.Modified;
            db.SaveChanges();
            return Updated(dtoOrder);
        }
        #endregion

 

To enable this OData method, we add the following code to the GenerateEntityDataModel() method in the WebApiConfig.cs file:

 

            // Register ODataOrders
            builder.EntitySet<DTOOrder>("ODataOrders");

 

Show Orders

 

image

The main screen shows the existing Orders and has a pager bar at the bottom to page through the results.

The following code shows the markup for the table:

 

<table style="width: auto;" 
        class="table table-hover table-bordered table-striped table-responsive table-condensed">
    <thead>
        <tr>
            <th width="20%">Edit</th>
            <th width="40%">User Name</th>
            <th width="40%">Order Date</th>
        </tr>
    </thead>
    <tbody ng-cloak>
        <tr ng-repeat="Order in vm.colOrders track by $index"
            id="{{OrderDetail.Id}}"
            class="active">
            <td align="center">
                <button type="button"
                        id="EditOrderDetailButton"
                        title="Edit"
                        class="btn btn-primary btn-xs glyphicon glyphicon-edit"
                        onclick="$('#EditOrderModal').modal('show');"
                        ng-click="vm.selectedOrder = Order ; vm.GetOrderDetails()"></button>
                <button type="button"
                        id="DeleteOrderButton"
                        title="Delete"
                        class="btn btn-danger btn-xs glyphicon glyphicon-trash"
                        ng-click="vm.selectedOrder = Order ; vm.DeleteOrder()"></button>
            </td>
            <td><span>{{Order.UserName}}</span></td>
            <td><span>{{Order.OrderDate}}</span></td>
        </tr>
    </tbody>
</table>

 

Note: The markup template also indicates that the GetOrderDetails() method will be called when an item is edited and the DeleteOrder() method will be called when an item is deleted. These methods will be covered later.

The markup for the paging control is as follows:

 

<uib-pagination boundary-links="true"
                    max-size="25"
                    items-per-page="5"
                    total-items="vm.totalOrderCount"
                    ng-model="vm.currentOrderPage"
                    ng-change="vm.GetAllOrders()"></uib-pagination>

 

As the markup indicates, when a change is made on the paging control (a user clicks one of the buttons) the GetAllOrders() method is called.

This is the AngularJs TypeScript code for that method:

 

        GetAllOrders() {
            // Construct the path to the OData end point
            // Handle paging
            var urlString: string = vm.baseUrl
                + "odata/ODataOrders?$count=true&$top=5&$skip=" + ((vm.currentOrderPage * 5)-5);
            // Call the OData end point
            var result: ng.IPromise<GetOrderArrayResults> = vm.$http({
                url: urlString,
                method: "GET"
            });
            // Process the result of the OData call
            // If successful, the Success method will be called
            // If not successful the Failure method will be called 
            result.then(Success, vm.Failure)
            // Handle a successful call
            function Success(results: GetOrderArrayResults) {
                // Fill the Orders collection
                vm.colOrders = results.data.value;
                // Handle Paging -- set total records 
                // This is returned because we set $count=true
                // in the OData query 
                vm.totalOrderCount = results.data['@odata.count'];
            }
        }

 

This method calls the following server-side GetODataOrders OData method.

Note: That while the AngularJs code just makes a call to ODataOrders, the server-side OData method is actually called GetODataOrders. The reason is that when calling an OData queryable method (one that implements IQueryable) you use the word Get in front of the method name.

This is the code for the GetODataOrders method:

    // GET: odata/ODataOrders
    [Authorize]
    [EnableQuery(PageSize = 100, AllowedQueryOptions = AllowedQueryOptions.All)]
    public IQueryable<DTOOrder> GetODataOrders()
    {
        var result = (from Orders in db.Orders
                        select new DTOOrder
                        {
                            Id = Orders.Id,
                            UserName = Orders.UserName,
                            OrderDate =
                            SqlFunctions.StringConvert((double)Orders.OrderDate.Month).TrimStart()
                            + "/"
                            + SqlFunctions.DateName("day", Orders.OrderDate)
                            + "/"
                            + SqlFunctions.DateName("year", Orders.OrderDate)
                        });
        // Only Administrators can see all records
        if (!this.User.IsInRole("Administrator"))
        {
            result = result
                        .Where(x => x.UserName == this.User.Identity.Name);
        }
        return result.AsQueryable();
    }

 

The key takeaway is that this method returns IQueryable so it can be queried. In our example, we pass to the method, a query that indicates how many records we want (“top” in the TypeScript code) and what page of the records we want (“skip” in the TypeScript code).

The following diagram shows how the paging works:

image

 

Edit Order

image

When an Order is edited, the following code is used for the markup of the button:

 

    <button type="button"
            id="EditOrderDetailButton"
            title="Edit"
            class="btn btn-primary btn-xs glyphicon glyphicon-edit"
            onclick="$('#EditOrderModal').modal('show');"
            ng-click="vm.selectedOrder = Order ; vm.GetOrderDetails()"></button>

 

The ng-click first sets the Order as the selectedOrder. It also calls the GetOrderDetails() method. This is the code for the method:

 

        GetOrderDetails() {
            // Get the name of the current user
            // Construct the path to the OData end point
            var urlString: string = vm.baseUrl
                + "odata/OrderDetailsForOrder(OrderId=" + vm.selectedOrder.Id + ")";
            var result: ng.IPromise<GetOrderDetailsArrayResults> = vm.$http({
                url: urlString,
                method: "POST"
            });
            // Process the result of the OData call
            // If successful, the Success method will be called
            // If not successful the Failure method will be called 
            result.then(Success, vm.Failure)
            function Success(results: GetOrderDetailsArrayResults) {
                // Clear the colOrderDetails
                vm.selectedOrder.colOrderDetails = new Array<OrderDetail>();
                // Loop through the OrderDetails
                results.data.value.forEach(OrderDetail => {
                    // Loop through the Products collection
                    vm.colProducts.forEach(Product => {
                        // Did we find the Product.Id 
                        // we are looking for?
                        if (Product.Id == OrderDetail.ProductId) {
                            // Set the Product
                            OrderDetail.Product = Product;
                        }
                    });
                    // Add the OrderDetail to the 
                    // colOrderDetails collection
                    vm.selectedOrder.colOrderDetails.push(OrderDetail);
                });
            }
        }

 

What is interesting about this code is that it calls the OData server-side method OrderDetailsForOrder([FromODataUri] int OrderId).

This method , unlike the GetODataOrders() method, covered earlier, returns data from the Order as well as the details for the Order.

This is the code for the method:

 

        // odata/OrderDetailsForOrder(OrderId=1)
        [ODataRoute("OrderDetailsForOrder(OrderId={OrderId})")]
        public IHttpActionResult OrderDetailsForOrder([FromODataUri] int OrderId)
        {
            Order ExistingOrder = db.Orders.Find(OrderId);
            if (ExistingOrder == null)
            {
                return StatusCode(HttpStatusCode.NotFound);
            }
            // Only Administrators or the orginal user
            // can delete the Order
            if (!this.User.IsInRole("Administrator"))
            {
                if (ExistingOrder.UserName != this.User.Identity.Name)
                {
                    throw new Exception(
                        String.Format("Record belongs to {0} and the curent user is {1}",
                        ExistingOrder.UserName,
                        this.User.Identity.Name));
                }
            }
            // Get the OrderDetails
            var result = (from OrderDetail in db.OrderDetails
                          where OrderDetail.OrderId == OrderId
                          select new DTOOrderDetail
                          {
                              Id = OrderDetail.Id,
                              ProductId = OrderDetail.Product.Id,
                              Quantity = OrderDetail.Quantity
                          }).ToList();
            return Ok(result);
        }

 

We also need to add the following code to the GenerateEntityDataModel() method in the WebApiConfig.cs file:

 

            // OrderDetailsForOrder function returns OrderDetails
            var OrderDetailsForOrder = builder.Function("OrderDetailsForOrder");
            OrderDetailsForOrder.ReturnsCollectionFromEntitySet<DTOOrderDetail>("DTOOrderDetails");
            OrderDetailsForOrder.Parameter<int>("OrderId");

 

Essentially, OData 4 allows you to create methods that can take whatever parameters you want, and return an entity of any shape you need.

Note: Another method is to use Entity Relations as covered here: http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/entity-relations-in-odata-v4

 

Delete

image

When an Order is deleted, the following code is used for the markup of the button:

 

        <button type="button"
                id="DeleteOrderButton"
                title="Delete"
                class="btn btn-danger btn-xs glyphicon glyphicon-trash"
                ng-click="vm.selectedOrder = Order ; vm.DeleteOrder()"></button>

 

The ng-click first sets the Order as the selectedOrder. It also calls the DeleteOrder() method. This is the code for the method:

 

    DeleteOrder() {
        var urlString: string = vm.baseUrl + "odata/ODataOrders(" + vm.selectedOrder.Id + ")";
        var result: any = vm.$http({
            url: urlString,
            method: "DELETE"
        });
        // Process the result of the OData call
        // If successful, the Success method will be called
        // If not successful the Failure method will be called 
        result.then(Success, vm.Failure)
        function Success(Order: any) {
            // Refresh Orders
            vm.GetAllOrders();
        }
    }

 

This method calls the following server-side OData method:

 

        // DELETE: odata/ODataOrders(1)
        [Authorize]
        public IHttpActionResult Delete([FromODataUri] int key)
        {
            Order ExistingOrder = db.Orders.Find(key);
            if (ExistingOrder == null)
            {
                return StatusCode(HttpStatusCode.NotFound);
            }
            // Only Administrators or the orginal user
            // can delete the Order
            if (!this.User.IsInRole("Administrator"))
            {
                if (ExistingOrder.UserName != this.User.Identity.Name)
                {
                    throw new Exception(
                        String.Format("Record belongs to {0} and the current user is {1}",
                        ExistingOrder.UserName,
                        this.User.Identity.Name));
                }
            }
            // Delete the Order
            // (and any Order Detail(s))
            db.Orders.Remove(ExistingOrder);
            // Save changes
            db.SaveChanges();
            return StatusCode(HttpStatusCode.NoContent);
        }

 

Special Thanks

A special thanks to Fatima Masroora and Eva Ng and Richard Waddell @qfeguy for their coding assistance.

Links

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

Documentation · OData - the Best Way to REST

Create an OData v4 Endpoint Using ASP.NET Web API 2.2 ...

TypeScrip.org

DefinitelyTyped

AngularJS — Superheroic JavaScript MVW Framework

Angular Applications with TypeScript

 

Download

The LightSwitch project is available at http://lightswitchhelpwebsite.com/Downloads.aspx

You must have Visual Studio 2015 (or higher) with LightSwitch installed to run the code (if you have Visual Studio Community Edition see How To Get Visual Studio LightSwitch For Free)

22 comment(s) so far...


Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

I recently created an OData V4 data service using the non-LightSwitch non-WCF Web API 2 approach and managed to consume the data using ODatajs for V4 and Datajs for the older LightSwitch V3 svc. Interesting to write the client using Notepad (well that and VSCode). Pain, pain and then more pain heaped upon pain. LightSwitch protected me from so many pain points. Still it has given me a greater appreciation of how OData delivers it's SQL data down the pipes. I just wish you'd written this great article a month or so back ;). Fantastic work again thx Michael.

Sorry I accidentally added this comment to one of you old blogs before I realised that it wasn't the blog that I wanted to comment on.

By Lloyd on   3/29/2016 7:07 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@Lloyd - Thanks :)

By Michael Washington on   3/29/2016 7:08 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

Michael,

I don't mean this question to be 'loaded', but is there any knowledge of if/when Lightswitch HTML will no longer be supported?

eg. Why did you write this article? In the sense that - is there a reason we need "to move from Visual Studio LightSwitch to the latest web technologies". To my beginner eye, there seems to be much more involved here compared to what Lightswitch can 'already' do.

After doing some googling, it seems that many think that Lightswitch has been abandoned by Microsoft. Would that be accurate to say?



By Kon on   3/29/2016 9:44 PM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@Kon - LightSwitch has not had any major updates in years. This is why I have explored other options on new projects. However, I continue to maintain my existing LightSwitch applications.

By Michael Washington on   3/29/2016 9:46 PM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

I think you'd meant to link: Creating User and Roles Administration Pages for an MVC5 Application

to: http://openlightgroup.com/Blog/TabId/58/PostId/189/UserRolesAdministration.aspx

but you never created the link.

By Lloyd on   3/31/2016 3:36 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@Lloyd - I fixed the link, Thanks :)

By Michael Washington on   3/31/2016 3:40 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

Great article Michael, thanks for it. May I ask if you have any thoughts on whether its possible to take the intrinsic database from an existing LS app and pre-generate a backend for an app like this, perhaps using Entity Framework database-first tools?

Also, do you have any thoughts on how to secure the OData service you're providing, to keep hackers from having fun with your data?

By larry q on   4/5/2016 8:19 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@larry q - The OData end point is secured by the [Authorize] attribute :) Also, RESTier can create part of a back-end See: http://www.odata.org/blog/restier-a-turn-key-framework-to-build-restful-service/

By Michael Washington on   4/5/2016 8:21 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

Michael, I know that today we are not supporting the lightswitch and you invested a lot of time with ls, and I invested.

I have many projects in lightswitch silverlight (desktop / web).

I would very much like your help to make an application on Microsoft for a migration tool for asp.net mvc.

and if you know any details of the future of the lightswitch. please, warn me.

thank you
Rodrigo

By rodrigo on   4/6/2016 6:35 PM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

I am sad to know that we can no longer rely on the lightswitch, a complete tool, with validation rules and permissions in a simple and fast way users.

and we have no substitute, with so many facilities without using codes

By rodrigo on   4/6/2016 6:35 PM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@Rodrigo - I have many LightSwitch projects that I will continue to maintain. As far as future projects I am using the method described in this article. I do not believe that a conversion tool would work, sorry.

By Michael Washington on   4/6/2016 6:36 PM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

Thanks for the correction Michael-- I didn't notice the [Authorize] attribute on the read method, just the POST and PUT ones. Are you looking at any 3rd party widgets to spice up the UI, that might work well with Angular? (The less CSS, html markup and Bootstrap code I have to add myself the better for everyone.)

By larry q on   4/7/2016 10:21 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@larry q - As far as spicing up the UI, I have been really happy with all that Bootstrap. When I need more, I use Wijmo because they are very deeply integrated with Angular. I have some articles on this site with sample code that show Wijmo controls and Angular.

By Michael Washington on   4/7/2016 10:24 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application


hello Michael , any news about the ls ?


bring it back !!!Michael !!!!

By rodrigo on   8/21/2016 3:00 PM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@rodrigo - No news, sorry.

By Michael Washington on   8/21/2016 3:00 PM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

Can this application be created in Visual Studio 2013? In VS 2013 I can't find NuGet Package Manager Console.

By Mike on   8/27/2016 7:22 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@Mike - I think NuGet Package Manager Console is called something else in VS2013. However, this tutorial is very VS2015 specific so just go ahead and download it because it is free :)

By Michael Washington on   8/27/2016 7:23 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

Any chance of popping this sample app on GitHub? It has a lot of useful elements that the community could use and contribute to as the migrations start to happen away from LightSwitch ...

By OzBobWa on   10/17/2016 8:38 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@OzBobWa - This is just a sample not a project that I plan to alter. I need to move on to other things. I may post an Angular2 version that would be a total re-write.

By Michael Washington on   10/17/2016 8:39 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

Perfectly understand. Good luck with the your AI/Data/Bots thing over at http://AiHelpWebsite.com

By OzBobWa on   10/17/2016 8:18 PM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

That's a lot of code for an extremely simple application, wouldn't one of the frameworks like Vaadin or Wavemaker be a simpler way to go? Plus it doesn't provide a way of migrating a LS application, just a way of rewriting it. It seems in many ways we have taken a giant leap backward in the last 20 years, I almost crave the days of Windows Forms when you could just drag and drop and double click for the code behind. Why should it be so hard to do that for the web environment? Come back Alan Cooper all is forgiven.

By Charlie on   10/18/2016 4:20 AM
Gravatar

Re: A OData 4 / AngularJs / TypeScript Sample Application

@Charlie - LightSwitch was a unique tooling framework in that it allowed me to always "code my way out of any problem". The only thing I can compare it to is Microsoft Access. So, for me, I recommend a recode rather than another tooling framework.

By Michael Washington on   10/18/2016 4:22 AM

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation