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(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.
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>();
}
public IEnumerable<phone> Get()
{
return phoneRepository.GetAll();
}
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"));
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>© @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">
-->
<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