Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Single Page Application with .NET MVC and AngularJS

0.00/5 (No votes)
14 Sep 2014 4  
Main content pages use AngularJS to display, and MVC .NET authorization is used for user validation

Introduction

This project is a code using AngularJs and .net MVC framework. Main function is displaying Phone List and Phone Detials.
This fuction is from tutorial of phoneCat on AngularJs home page. And I added user management module with AngularJS. Usually, AngularJS is used for single page application, but there's lots of adventages if we use MVC together. For example, we can reduce the effort on user authentification and authorization module in AngularJS.
We can move user auth modules on Server side, and it make pretty easy to manage Security, Menu management, etc. You can see the demo site at here.

Background

All background is very common, so you can google it and find it in codeproject site. If you are not familiar with AngularJS, please visit the AngularJS site.

 

 

Connection String Configuration

From MVC4, user management module uses DefaultConnection for storing user data. In this sample proejct, I will use SpaContext naming for database connection.
I want to manage all data in one database, so, first of all, I congifured Web.Config for connection string, and changed Connection String from DefaultConnection to SpaContext in Models\IdentityModels.cs

<connectionStrings>
  <add name="SpaContext" 
       connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\SinglePageApp.Web.mdf;Initial Catalog=SinglePageApp.Web;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>
public class ApplicationDbContext : IdentityDbContext<applicationuser>
  {
      public ApplicationDbContext()
          : base("SpaContext")
      {
      }
  }
</applicationuser>

DI Configuration

For Depedency Injection configuration, I get the code snipped in DependencyResolution\IoC.cs file

public static class IoC {
    public static IContainer Initialize() {

        var container = new Container(x =>
            {
                x.Scan(scan =>
                {
                    scan.TheCallingAssembly();
                    scan.WithDefaultConventions();
                });
                //                x.For<iexample>().Use<example>();
                x.For(typeof(IRepository<>)).Use(typeof(Repository<>));
                x.For<iunitofwork>().Use<unitofwork>();
            });

        return container;
    }
}
</unitofwork></iunitofwork></example></iexample>

Authentification Setting

In this sample, I will disallow the users to access phone details page. Only logged-in user can access the phone detail page.
So, open App_Start/Startup.Auth.cs file, change the redirect url from /Account/Login to /Account/LoginPartial. In the View explanation, I will explain why I created another Login View.


// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/LoginPartial")                
});

Controllers

From HomeController, I added two Actions for AngularJS service, PhoneList and PhoneDetila. As I said, PhoneDetail Controller has Authorize Attribute to restrict user access.

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult About()
    {
        ViewBag.Message = "Your application description page.";

        return View();
    }

    public ActionResult Contact()
    {
        ViewBag.Message = "Your contact page.";

        return View();
    }

    public ActionResult PhoneList()
    {
        return View();
    }

    [Authorize]
    public ActionResult PhoneDetail()
    {
        return View();
    }
}

Second Controller,PhoneController, is for AngularJS RESTful service. In this example, I didn't any user authentification logic, but it depends on developers coding style. Next article, I will show you how to restrict to access RESTful service from AngularJS.

public class PhoneController : ApiController
{
    private IUnitOfWork unitOfWork;
    private IRepository<phone> phoneRepository;

    public PhoneController(IUnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
        phoneRepository = this.unitOfWork.Repository<phone>();
    }

    // GET api/<controller>
    public IEnumerable<phone> Get()
    {
        return phoneRepository.GetAll();
    }
    
    // GET api/<controller>/5
    public PhoneDetailDTO Get(string id)
    {
        return JsonConvert.DeserializeObject<phonedetaildto>(phoneRepository.GetById(id).PhoneDetail.Json);            
    }
     
}	
</phonedetaildto></controller></phone></controller></phone></phone>

In the AccountController, I added two Actions,LoginPartial and RegisterPartial. If a user try to access PhoneDetail Controller, Framework will redirect to /Account/Login action. Because of this, ng-view tag will have whole page with _Layout.chtml master page A result screen shot is like this.


As you can see, there're two footer. Other content's are layered on by themself. You can check this on with F12 developer mode source view. To avoid this problem, I added two partial View not containing Layout.chtml master page.

[AllowAnonymous]
public ActionResult LoginPartial()
{
    return View();
}

[AllowAnonymous]
public ActionResult RegisterPartial()
{
    return View();
}     

Data Transfer Object

Because the PhoneDetail data is stored in database as full string, we need to define DTO for RESTful service.

public class PhoneDetailDTO
{
    public string additionalFeatures { get; set; }
    public Android android { get; set; }
    public List<string> availability { get; set; }
    public Battery battery { get; set; }
    public Camera camera { get; set; }
    public Connectivity connectivity { get; set; }
    public string description { get; set; }
    public Display display { get; set; }
    public Hardware hardware { get; set; }
    public string id { get; set; }
    public List<string> images { get; set; }
    public string name { get; set; }
    public SizeAndWeight sizeAndWeight { get; set; }
    public Storage storage { get; set; }
}

public class Android
{
    public string os { get; set; }
    public string ui { get; set; }
}

public class Battery
{
    public string standbyTime { get; set; }
    public string talkTime { get; set; }
    public string type { get; set; }
}

public class Camera
{
    public List<string> features { get; set; }
    public string primary { get; set; }
}

public class Connectivity
{
    public string bluetooth { get; set; }
    public string cell { get; set; }
    public bool gps { get; set; }
    public bool infrared { get; set; }
    public string wifi { get; set; }
}

public class Display
{
    public string screenResolution { get; set; }
    public string screenSize { get; set; }
    public bool touchScreen { get; set; }
}

public class Hardware
{
    public bool accelerometer { get; set; }
    public string audioJack { get; set; }
    public string cpu { get; set; }
    public bool fmRadio { get; set; }
    public bool physicalKeyboard { get; set; }
    public string usb { get; set; }
}

public class SizeAndWeight
{
    public List<string> dimensions { get; set; }
    public string weight { get; set; }
}

public class Storage
{
    public string flash { get; set; }
    public string ram { get; set; }
}
</string></string></string></string>

Views

Before creating partial view, I added scripts and css files to Bundle file

public static void RegisterBundles(BundleCollection bundles)
{
    bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                "~/Scripts/jquery-{version}.js"));

    bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                "~/Scripts/jquery.validate*"));

    bundles.Add(new ScriptBundle("~/bundles/angularjs").Include(
                "~/app/components/angular/angular.js",
                "~/app/components/angular-route/angular-route.js",
                "~/app/components/angular-resource/angular-resource.js",
                "~/js/app.js",
                "~/js/controllers.js",
                "~/js/filters.js",
                "~/js/services.js"));

    // Use the development version of Modernizr to develop with and learn from. Then, when you're
    // ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
    bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                "~/Scripts/modernizr-*"));

    bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
              "~/Scripts/bootstrap.js",
              "~/Scripts/respond.js"));

    bundles.Add(new StyleBundle("~/Content/css").Include(
              "~/Content/bootstrap.css",
              "~/Content/site.css",
              "~/Content/app.css"));
}

This is Layout.chtml for main layout.

<!DOCTYPE html>
<html ng-app="phonecatApp" lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET / AngularJS Application</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
 
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("ASP.NET / AngularJS", "Index", "Home", null, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">

                @Html.Partial("_LoginPartial")
            </div>
        </div>
    </div>
    <div class="container body-content">
        
        @RenderBody()

        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @Scripts.Render("~/bundles/angularjs")
    @RenderSection("scripts", required: false)
</body>
</html>

This is Home/Index view to diplay main page. The main page is PhoneList. It only has div tag with ng-view directive.

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
    ViewBag.Title = "Home Page";    
}

<div ng-view></div>

This is PhoneList View page. You can see the detail explanation for directives on AngularJS page.

@{
    Layout = null;
}

<img ng-src="{{mainImageUrl}}" class="phone">

<h1>{{phone.name}}</h1>

<p>{{phone.description}}</p>

<ul class="phone-thumbs">
    <li ng-repeat="img in phone.images">
        <img ng-src="{{img}}" ng-click="setImage(img)">
    </li>
</ul>

<ul class="specs">
    <li>
        <span>Availability and Networks</span>
        <dl>
            <dt>Availability</dt>
            <dd ng-repeat="availability in phone.availability">{{availability}}</dd>
        </dl>
    </li>
    <li>
        <span>Battery</span>
        <dl>
            <dt>Type</dt>
            <dd>{{phone.battery.type}}</dd>
            <dt>Talk Time</dt>
            <dd>{{phone.battery.talkTime}}</dd>
            <dt>Standby time (max)</dt>
            <dd>{{phone.battery.standbyTime}}</dd>
        </dl>
    </li>
    <li>
        <span>Storage and Memory</span>
        <dl>
            <dt>RAM</dt>
            <dd>{{phone.storage.ram}}</dd>
            <dt>Internal Storage</dt>
            <dd>{{phone.storage.flash}}</dd>
        </dl>
    </li>
    <li>
        <span>Connectivity</span>
        <dl>
            <dt>Network Support</dt>
            <dd>{{phone.connectivity.cell}}</dd>
            <dt>WiFi</dt>
            <dd>{{phone.connectivity.wifi}}</dd>
            <dt>Bluetooth</dt>
            <dd>{{phone.connectivity.bluetooth}}</dd>
            <dt>Infrared</dt>
            <dd>{{phone.connectivity.infrared | checkmark}}</dd>
            <dt>GPS</dt>
            <dd>{{phone.connectivity.gps | checkmark}}</dd>
        </dl>
    </li>
    <li>
        <span>Android</span>
        <dl>
            <dt>OS Version</dt>
            <dd>{{phone.android.os}}</dd>
            <dt>UI</dt>
            <dd>{{phone.android.ui}}</dd>
        </dl>
    </li>
    <li>
        <span>Size and Weight</span>
        <dl>
            <dt>Dimensions</dt>
            <dd ng-repeat="dim in phone.sizeAndWeight.dimensions">{{dim}}</dd>
            <dt>Weight</dt>
            <dd>{{phone.sizeAndWeight.weight}}</dd>
        </dl>
    </li>
    <li>
        <span>Display</span>
        <dl>
            <dt>Screen size</dt>
            <dd>{{phone.display.screenSize}}</dd>
            <dt>Screen resolution</dt>
            <dd>{{phone.display.screenResolution}}</dd>
            <dt>Touch screen</dt>
            <dd>{{phone.display.touchScreen | checkmark}}</dd>
        </dl>
    </li>
    <li>
        <span>Hardware</span>
        <dl>
            <dt>CPU</dt>
            <dd>{{phone.hardware.cpu}}</dd>
            <dt>USB</dt>
            <dd>{{phone.hardware.usb}}</dd>
            <dt>Audio / headphone jack</dt>
            <dd>{{phone.hardware.audioJack}}</dd>
            <dt>FM Radio</dt>
            <dd>{{phone.hardware.fmRadio | checkmark}}</dd>
            <dt>Accelerometer</dt>
            <dd>{{phone.hardware.accelerometer | checkmark}}</dd>
        </dl>
    </li>
    <li>
        <span>Camera</span>
        <dl>
            <dt>Primary</dt>
            <dd>{{phone.camera.primary}}</dd>
            <dt>Features</dt>
            <dd>{{phone.camera.features.join(', ')}}</dd>
        </dl>
    </li>
    <li>
        <span>Additional Features</span>
        <dd>{{phone.additionalFeatures}}</dd>
    </li>
</ul>

This is PhoneDetail View page. You can see the detail explanation for directives on AngularJS page.

@{
    Layout = null;
}

<div class="container-fluid">
    <div class="row">
        <div class="col-md-2">
            <!--Sidebar content-->
            Search: <input ng-model="query">
            Sort by:
            <select ng-model="orderProp">
                <option value="name">Alphabetical</option>
                <option value="age">Newest</option>
            </select>

        </div>
        <div class="col-md-10">
            <!--Body content-->

            <ul class="phones">
                <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
                    <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
                    <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
                    <p>{{phone.snippet}}</p>
                </li>
            </ul>

        </div>
    </div>
</div>	

This is key point View in this project. When a user clicks the Phone Detail link without authentification, MVC framework will redirect the user to Login Action. (/Account/LoginPartial)
If you successfully logged in, the LoginPartial Action will redirect the user to phone detail according to ReturnUrl parameter.
Because returnUrl parameter is set automatically from framework, and the returnUrl will be /Home/PhoneDetail action
This is the problem. When a user redirec to /Home/PhoneDetail, the user will see html tags, NOT phone detail information
So, I added returnUrl parameter when submitting the page using Javascript function. If the returnUrl parameter is added on /Home/PhoneDetail page, the returnUrl paramter will be redirect as well.
As a result, you can see the phonedetail page.
If you want to see what happens if the page is submitted without returnUrl, just remove '?returnUrl=' + encodeURIComponent(returnUrl) this part in Javascript function.

@model SinglePageApp.Web.Models.LoginViewModel
@{
    ViewBag.Title = "Log in";
    Layout = null;
}


<h2>@ViewBag.Title.</h2>

<div class="row">
    <div class="col-md-8">
        <section id="loginForm">
            @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
            {
                @Html.AntiForgeryToken()
                <h4>Use a local account to log in.</h4>
                <hr />
                @Html.ValidationSummary(true)
                <div class="form-group">
                    @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.UserName)
                    </div>
                </div>
                <div class="form-group">
                    @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.Password)
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <div class="checkbox">
                            @Html.CheckBoxFor(m => m.RememberMe)
                            @Html.LabelFor(m => m.RememberMe)
                        </div>
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <input id="loginButton" type="submit" value="Log in" class="btn btn-default" />
                    </div>
                </div>
                <p>
                    @Html.ActionLink("Register", "Register") if you don't have a local account.
                </p>
            }
        </section>
    </div>
    <div class="col-md-4">
        <section id="socialLoginForm">
            @Html.Partial("_ExternalLoginsListPartial", new { Action = "ExternalLogin", ReturnUrl = ViewBag.ReturnUrl })
        </section>
    </div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

<script type="text/javascript">

    $(document).ready(function () {
        $("#loginButton").click(function (e) {
            e.preventDefault();
            var returnUrl = $(location).attr('pathname') + $(location).attr('hash');
            var formAction = $("form").attr("action") + '?returnUrl=' + encodeURIComponent(returnUrl);
            $("form").attr("action", formAction);
            $("form").submit();
        });
    });

</script>

This is RegisterPartialView code. It's copied form RegisterView.

@model SinglePageApp.Web.Models.RegisterViewModel
@{
    ViewBag.Title = "Register";
    Layout = null;
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Create a new account.</h4>
    <hr />
    @Html.ValidationSummary()
    <div class="form-group">
        @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Register" />
        </div>
    </div>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}


History

September 11, 2014 - Posted initially

September 14, 2014 - Broken demo link update

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here