Click here to Skip to main content
11,490,003 members (57,881 online)
Click here to Skip to main content

ASP.NET MVC Single Page App with Upida (Frontend/AngularJS)

, 12 Jan 2015 CPOL 42.1K 1.7K 38
Rate this:
Please Sign up or sign in to vote.
This article shows how to create a single page web applciation using AngularJS. It will be helpfull for people willing to study angular and mvvm practices.

Introduction   

In the previous article I demonstrated how to create a JSON-powered web back-end using WebAPI and Upida.Net. In this article I will show some basic tips on how to create a single page front end side using Angular.js. If you are not familiar with basics of AngularJS, then please refer to this YouTube channel.

Background  

Let's assume that we already have working back-end, which contains two controllers - one MVC controller, that provides HTML views, and one WebAPI controller, that gives us handy REST JSON api.

public class ClientController : System.Web.Mvc.Controller
{
    public ActionResult List()
    {
        return this.View();
    }
 
    public ActionResult Create()
    {
        return this.View();
    }
 
    public ActionResult Edit()
    {
        return this.View();
    }
}
public class ClientController : System.Web.Http.ApiController
{
    public IClientService ClientService { get; set; }

    public Client GetById(int id)
    {
        return this.ClientService.GetById(id);
    }

    public IList<client> GetAll()
    {
        return this.ClientService.GetAll();
    }

    public void Save(Client item)
    {
        this.ClientService.Save(item);
    }

    public void Update(Client item)
    {
        this.ClientService.Update(item);
    }

    [HttpPost]
    public void Delete(int id)
    {
        this.ClientService.Delete(id);
    }
}</client>

Both controllers look pretty simple and workable. Please refer to my previous article to find out how I created back-end. My goal is creating the front-end for these two controllers. The front-end must be a single-page browser application (SPA). Many people think, that SPA is not a useful technique, because it does not produce links to different pages and SPA requires you so much effort, that it is not worth it. I will show you that, using AngularJS you can create SPA as simple as creating multiple-pages application, and all the benefits of the multiple-page apps will be preserved in SPA. 

Basic structure

The most important part of every single page application (SPA) is its structure. We are going to have 3 views: "List of Clients", "Create Client" and "Edit Client". This actually means that we will have three AngularJS controllers for these views - clientListController.js, clientCreateController.js, and clientEditController.js. The "vendor" folder contains all third-party libraries. We also have custom filters. Usually this kind of structure also has "directives" and "services" folder, but in this simple SPA I am not using any custom directives and services.  

The fundamental part of the front-end is the app.js file located in the root of the js folder. Let's analyze its contents. 

var myclients = myclients || {};
myclients.app = angular.module("myclients", ["ngRoute", "upidamodule"]);
 
myclients.app.config(function ($routeProvider) {
    $routeProvider
        .when("/", {
            templateUrl: "client/list",
            controller: "clientListController"
        })
        .when("/client/list", {
            templateUrl: "client/list",
            controller: "clientListController"
        })
        .when("/client/create", {
            templateUrl: "client/create",
            controller: "clientCreateController"
        })
        .when("/client/edit/:id", {
            templateUrl: "client/edit",
            controller: "clientEditController"
        })
        .otherwise({
            templateUrl: "home/notfound"
        });
});
 
$upida.settings.baseUrl = "/api/";

In the first line I define main namespace of my application. Then, using AngularJS, I create an instance of my main module. This is a common task to every angular application. And this module is dependent on two different modules: ngRoute and upidamodule, which both come as independent reusable angular modules. The most interesting part is in the middle. As you see, I define several routes here. This is actually the core of any front-end. Simply-saying - here I tell AngularJS - which js controller and which view should be turned on, depending on the URL in the browser. Every moment, my application will have only one active view and controller, and this will depend on the URL entered in the browser window, that is it.  For ex. if I type url: client/list, then client/list view and clientListController will be loaded active.

The last part of the app.js is dedicated to Upida.Net. I use it because it makes my application easier to develop and support. You can refer to my previous article to find out more about Upida.Net. I tell Upida, that my WebAPI controllers are located in the /api/ subfolder. 

JS Controllers  

Now, let's take a look at controllers. The first one is clientListController.js.

myclients.app.controller(
    "clientListController", ["$scope", "upida",
    function ($scope, upidaService) {
 
    $scope.clientRows = new Array();
 
    $scope.ClientRow = function (id) {
        this.id = id;
        this.name = null;
        this.lastname = null;
        this.age = null;
        this.logins = new Array();
    };
 
    $scope.loadClients = function() {
        upida.get("client/getall")
        .then(function (items) {
            angular.forEach(items, function (p, i) {
                var row = new $scope.ClientRow(p.id);
                row.name = p.name;
                row.lastname = p.lastname;
                row.age = p.age;
                angular.forEach(p.logins, function (q, j) {
                    row.logins.push(q.name);
                });
                $scope.clientRows.push(row);
            });
        });
    };

   $scope.onDelete = function (clientId) {
      upida.post("client/delete/" + clientId, null)
      .then(function () {
         $scope.loadClients();
      });
   };
 
    $scope.$on("$routeChangeSuccess", function () {
        upida.setScope($scope);
        $scope.loadClients();
    });
}]);

As you see, I refer to the myclients.app module created in the first two lines of the app.js file. My controller is named clientListController and is injected with 2 variables: $scope, and upida. In the body of the controller I define $scope.clientRows - which is the model of my view - list of clients from the database. Interaction with back-end is made in the $scope.loadClients method, which uses upida service to call WebAPI controllers. The reason why I use upida service instead of direct angular ($http) call is - upida manages automatically, if my backend fails and will display error messages in the correct place in my view (based on mainerror and errorkey directives). When my WebAPI returns list of Clients, I populate them into the $scope.clientRows field.

The last part of my controller is the most important. I attach handler to the $routeChangeSuccess event. This event fires every time, when this controller becomes active, i.e. every time user navigates to the "List of Clients" view, this event gets fired. In my case, when this event is fired, the $scope.loadClients method  is called, and data is extracted from backend. 

Other controllers work about the same. They are injected with $location variable, which is used to navigate between view in AngularJS SPA. For example, the clientCreateController.js:

myclients.app.controller(
    "clientCreateController",
    ["$scope", "$location", "upida", function ($scope, $location, upida) {
 
    $scope.name = null;
    $scope.lastname = null;
    $scope.age = null;
    $scope.loginRows = new Array();
 
    $scope.LoginRow = function () {
        this.name = null;
        this.password = null;
        this.enabled = false;
    };
 
    $scope.onRemoveLoginClick = function (item) {
        var index = $scope.loginRows.indexOf(item);
        $scope.loginRows.splice(index, 1);
    };
 
    $scope.onAddLoginClick = function () {
        var row = new $scope.LoginRow();
        $scope.loginRows.push(row);
    };
 
    $scope.onSave = function () {
        var data = {};
        data.name = $scope.name;
        data.lastname = $scope.lastname;
        data.age = $scope.age;
        data.logins = new Array();
        angular.forEach($scope.loginRows, function (p, i) {
            var item = {};
            item.name = p.name;
            item.password = p.password;
            item.enabled = p.enabled;
            data.logins.push(item);
        });
        upida.post("client/save", data)
        .then(function () {
            $location.path("client/list");
        });
    };
 
    $scope.$on("$routeChangeSuccess", function () {
        $scope.onAddLoginClick();
    });
}]);

Take a look at the $scope.onSave method. It fires when user clicks on the Save button of the Client Create view. It collects all user-entered data into a big data variable, and sends it as JSON to WebAPI controller, using upida service. If data is saved successfully, the $location.path() method is called and user gets navigated to the List of Clients view again. If backend fails or if backend validation is failed, the upida service will handle this issue and all validation issues will be displayed in the view in correct places, depending on the upida directives (later in this article you will see how Upida handles these failures).

The clientEditController.js works the same way, you can see it in the source code.

HTML Views

As you already know, there are only three views - List of Clients, Create Client, and Edit Client. If you are not familiar with AngularJS, then please refer to this youtube channel. Let's take a look at the List view.

<a href="#/client/create">NEW CLIENT</a>
<h2>Clients</h2>
<span class="error" up-error-key up-error-body></span>
<hr />
<table border="1" style="width: 50%;">
<thead>
    <tr class="head">
        <th>ID</th>
        <th>NAME</th>
        <th>LASTNAME</th>
        <th>AGE</th>
        <th>LOGINS</th>
        <th></th>
    </tr>
</thead>
<tbody>
    <tr ng-repeat="row in clientRows">
        <td ng-bind="row.id"></td>
        <td ng-bind="row.name"></td>
        <td ng-bind="row.lastname"></td>
        <td ng-bind="row.age"></td>
        <td>
            <div ng-repeat="login in row.logins">
                <span ng-bind="login"></span><br />
            </div>
        </td>
        <td>
            <a ng-href="#/client/edit/{{row.id}}">Edit</a>
        </td>
        <td>
            <input type="button" ng-click="onDelete(row.id)" value="Delete" />
        </td>
    </tr>
</tbody>
</table>

For those, who are familiar with AngularJS, it would be pretty simple to understand this code. This view is tied together with clientListController.js. Basically, this code refers to the $scope.clientRows field from the controller's code, and displays list of clients as HTML table.

The most interesting moment here is links. Take a look at the links:

<a href="#/client/create">NEW CLIENT</a>

As you see, the URL starts with a # sign, which actually means that if you click the link, the browser would not reload the page. Instead, AngularJS will capture this event, and will switch the active view and active controller, based on the routes registered in the app.js file. For ex. if you click the NEW CLIENT link, AngularJS will replace contents of the current view with contents of the Create Client view, and active controller will become - clientCreateController.js.

The other interesting moment is in the top of the view:

<span class="error" up-error-key up-error-body></span> 

This span is controlled by the up-error-key and up-error-body directives from Upida. Which actually means, that when failure response is arrived from backend server to browser, all key-less errors will be displayed in the body of the span. Sometimes you have to use key-less (or path-less) error messages, for ex. "Unexpected failure" message or some more specific message - "You cannot delete this client". The best thing about key-less failure messages is that they don't require any JSON data to be posted to server. In addition to key-less messages  you are always free to define your custom keys, for ex. you can define "server_error" key (path), then you can register failures with this key to validation context, and then you can use the up-error-key directive to display that failure message anywhere on your form. In this example I use key-less messages for unhandled exceptions and simply display them in the top of the each form. I also use key-less messages to inform that user cannot delete a client due to some reason (See deleting client in the ClientService).

Now take a look at the Edit Client link:

<a ng-href="#/client/edit/{{row.id}}">Edit</a> 

As you see, this link navigates to the Edit Client view, it also contains id of the client. The editClientController must know how to extract this id from the link. Take a look at the app.js file, how the route for the Edit Client view is defined.

.when("/client/edit/:id", {
    templateUrl: "client/edit",
    controller: "clientEditController"
}) 

AngularJS routing mechanism is able to handle url parameters. And the clientEditController can access them using the $routeParams service. The $routeParams service  must be injected into controller the same way as other services. Please, see the source code to get more details.

Now, let's take a look at the Create Client view.

<a href="#/client/list">ALL CLIENTS</a>|
<h2>New Client</h2>
<span class="error" up-error-key up-error-body></span>
<hr />
<table>
<tr>
    <td>Name</td>
    <td>
        <input type="text" ng-model="name" />
        <span class="error" up-error-key="name" up-error-body></span>
    </td>
</tr>
<tr>
    <td>Last name</td>
    <td>
        <input type="text" ng-model="lastname" />
        <span class="error" up-error-key="lastname" up-error-body></span>
    </td>
</tr>
<tr>
    <td>Age</td>
    <td>
        <input type="text" ng-model="age" />
        <span class="error" up-error-key="age" up-error-body></span>
    </td>
</tr>
<tr>
    <td>Logins</td>
    <td>
        <table>
          <thead>
            <tr>
            <th>Login</th>
            <th>Password</th>
            <th>Enabled</th>
            <th></th>
            </tr>
          </thead>
          <tbody>
            <tr ng-repeat="row in loginRows">
            <td>
                <input type="text" ng-model="row.name" />
                <span class="error" up-error-key="{{'logins['+ $index + '].name'}}" up-error-body></span>
            </td>
            <td>
                <input type="text" ng-model="row.password" />
                <span class="error" up-error-key="{{'logins['+ $index + '].password'}}" up-error-body></span>
            </td>
            <td>
                <input type="checkbox" ng-model="row.enabled" />
                <span class="error" up-error-key="{{'logins['+ $index + '].enabled'}}" up-error-body></span>
            </td>
            <td>
                 <input type="button" ng-click="onRemoveLoginClick(row)" value="Delete" />
            </td>
            </tr>
          </tbody>
        </table>
        <div style="padding-bottom: 0.5em;">
             <input type="button" ng-click="onAddLoginClick()" value="Add" />
             <span class="error" up-error-key="logins" up-error-body></span>
        </div>
    </td>
</tr>
</table>
<hr />
<input type="button" ng-click="onSave()" value="Save" />

Every input element is tied to corresponding controller's $scope field using ng-model Angular directive. Every input element is followed by error message span.

<input type="text" ng-model="name" />
<span class="error" up-error-key="name" up-error-body></span> 

or  like this 

<tr ng-repeat="row in loginRows">
<td>
    <input type="text" ng-model="row.name" />
    <span class="error" up-error-key="{{'logins['+ $index + '].name'}}" up-error-body></span> 

As you see, the span is decorated with up-error-key and up-error-body directives, which come from Upida. Which basically means, that if validation fails for the name field the body of the span with up-error-key="name" will contain error message. The second case looks strange, but it is pretty simple too. up-error-key="{{'logins['+ $index + '].name'}}" this means if logins[0].name field is failed - then error message will be placed in correct place. Ther is also up-error-class directive, which is, when used together with the up-error-key directive can change elements CSS class if validation fails. 

View container

Well, this is basically it. We have views, we have controllers, we have app.js which configures everything. The last step is to create a page, the single page. The page, that hosts all views and controllers. This page will be displayed in browser when user navigates to the root of the web applciation. My container page is located here: views/home/Index.cshtml. It looks very simple: 

<!DOCTYPE html>
<html ng-app="myclients">
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
   <title>MyClients Single Page Angular Application</title>
 
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/vendor/angular.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/vendor/angular-route.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/vendor/upida.angular.js")"></script>
 
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/app.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/filters/idtext.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/filters/datetime.js")"></script>
 
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/controllers/clientCreateController.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/controllers/clientListController.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/controllers/clientEditController.js")"></script>
   <link href="@Url.Content("~/Resources/css/style.css")" rel="stylesheet"/>
</head>
<body>
    <ng-view>
    </ng-view>
</body>
</html>

First of all I have to define application module using the ng-app directive. My module is called myclients, this is the module refered in the app.js file. Then I have to include angular.js, upida.angular.js, and all controllers, services, directives and filters.

The body element contains just one tag - <ng-view>, as you have allready guessed, AngularJS will replace the contents of this tag by currently active view. And that is it.

Conclusion

Using AngularJS you can create single-page applications without any extra effort. Upida.Net makes it even more simple by managing validation routines.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

vladimir husnullin
Software Developer
United States United States
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pin
Jerry72217-Jan-15 9:28
professionalJerry72217-Jan-15 9:28 
QuestionComplete example Pin
Member 1077312219-Jun-14 12:32
memberMember 1077312219-Jun-14 12:32 
AnswerRe: Complete example Pin
vladimir husnullin20-Jun-14 2:08
membervladimir husnullin20-Jun-14 2:08 
GeneralRe: Complete example Pin
Member 1077312225-Jun-14 6:46
memberMember 1077312225-Jun-14 6:46 
GeneralRe: Complete example Pin
vladimir husnullin25-Jun-14 11:26
membervladimir husnullin25-Jun-14 11:26 
SuggestionFrontend and backend with C# ? Pin
Wladis28-Mar-14 21:40
memberWladis28-Mar-14 21:40 
Questionmemory management Pin
lakhdarr12-Feb-14 10:00
memberlakhdarr12-Feb-14 10:00 
AnswerRe: memory management Pin
vladimir husnullin12-Feb-14 11:57
membervladimir husnullin12-Feb-14 11:57 
GeneralRe: memory management Pin
lakhdarr13-Feb-14 8:58
memberlakhdarr13-Feb-14 8:58 
GeneralRe: memory management Pin
vladimir husnullin13-Feb-14 12:13
membervladimir husnullin13-Feb-14 12:13 
SuggestionAngularJS SPA Template for Visual Studio Pin
NavinBiz7-Dec-13 5:05
memberNavinBiz7-Dec-13 5:05 
QuestionAzure Host Pin
Sampath Lokuge28-Nov-13 22:22
memberSampath Lokuge28-Nov-13 22:22 
GeneralRe: Azure Host Pin
vladimir husnullin29-Nov-13 9:25
membervladimir husnullin29-Nov-13 9:25 
GeneralRe: Azure Host Pin
Sampath Lokuge29-Nov-13 20:05
memberSampath Lokuge29-Nov-13 20:05 
GeneralMy vote of 5 Pin
M Rayhan28-Nov-13 22:03
memberM Rayhan28-Nov-13 22:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150520.1 | Last Updated 12 Jan 2015
Article Copyright 2013 by vladimir husnullin
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid