65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 .NET MVC 和 AngularJS 的单页应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (15投票s)

2014 年 9 月 14 日

CPOL

3分钟阅读

viewsIcon

79212

downloadIcon

4639

主内容页面使用 AngularJS 显示,MVC .NET 授权用于用户验证

引言

此项目是使用 AngularJs 和 .net MVC 框架的代码。主要功能是显示电话列表和电话详情。
此功能来自 AngularJs 主页上的 phoneCat 教程。 我添加了带有 AngularJS 的用户管理模块。 通常,AngularJS 用于单页应用程序,但如果我们一起使用 MVC,则有很多优势。 例如,我们可以减少 AngularJS 中用户身份验证和授权模块的工作量。
我们可以将用户身份验证模块移动到服务器端,这使得管理安全性、菜单管理等变得非常容易。您可以在这里看到演示站点。

背景

所有背景都非常常见,所以您可以在 codeproject 站点上搜索并找到它。如果您不熟悉 AngularJS,请访问 AngularJS 站点。

 

 

连接字符串配置

从 MVC4 开始,用户管理模块使用 DefaultConnection 来存储用户数据。 在此示例项目中,我将使用 SpaContext 命名作为数据库连接。
我想在一个数据库中管理所有数据,所以首先,我为连接字符串配置了 Web.Config,并将 Models\IdentityModels.cs 中的连接字符串从 DefaultConnection 更改为 SpaContext

<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
  {
      public ApplicationDbContext()
          : base("SpaContext")
      {
      }
  }

DI 配置

对于依赖注入配置,我在 DependencyResolution\IoC.cs 文件中获取了代码片段

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

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

        return container;
    }
}

身份验证设置

在此示例中,我将禁止用户访问电话详细信息页面。 只有登录的用户才能访问电话详细信息页面。
所以,打开 App_Start/Startup.Auth.cs 文件,将重定向 URL 从 /Account/Login 更改为 /Account/LoginPartial。 在 View 解释中,我将解释为什么我创建了另一个登录 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")                
});

控制器

从 HomeController 中,我为 AngularJS 服务添加了两个 Actions,PhoneList 和 PhoneDetila。 正如我所说,PhoneDetail Controller 具有 Authorize Attribute 来限制用户访问。

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();
    }
}

第二个 Controller,PhoneController,用于 AngularJS RESTful 服务。 在此示例中,我没有任何用户身份验证逻辑,但这取决于开发人员的编码风格。 下一篇文章,我将向您展示如何限制从 AngularJS 访问 RESTful 服务。

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

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

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

在 AccountController 中,我添加了两个 Actions,LoginPartial 和 RegisterPartial。 如果用户尝试访问 PhoneDetail Controller,框架将重定向到 /Account/Login 操作。 因此,ng-view 标签将具有包含 _Layout.chtml 母版页的整个页面。结果屏幕截图如下所示。


正如您所看到的,有两个页脚。 其他内容由它们自己分层。 您可以使用 F12 开发人员模式源视图来检查这一点。 为了避免这个问题,我添加了两个不包含 Layout.chtml 母版页的部分 View。

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

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

数据传输对象

因为 PhoneDetail 数据以完整字符串的形式存储在数据库中,所以我们需要为 RESTful 服务定义 DTO。

public class PhoneDetailDTO
{
    public string additionalFeatures { get; set; }
    public Android android { get; set; }
    public List 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 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 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 dimensions { get; set; }
    public string weight { get; set; }
}

public class Storage
{
    public string flash { get; set; }
    public string ram { get; set; }
}

视图

在创建部分视图之前,我将脚本和 css 文件添加到 Bundle 文件

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"));
}

这是主布局的 Layout.chtml。

<!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>

这是 Home/Index 视图来显示主页。 主页是 PhoneList。 它只有带有 ng-view 指令的 div 标签。

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

<div ng-view></div>

这是 PhoneList View 页面。 您可以在 AngularJS 页面上看到有关指令的详细说明。

@{
    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>

这是 PhoneDetail View 页面。 您可以在 AngularJS 页面上看到有关指令的详细说明。

@{
    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>	

这是此项目中的关键点 View。 当用户在没有身份验证的情况下单击 Phone Detail 链接时,MVC 框架会将用户重定向到 Login Action。 (/Account/LoginPartial)
如果您成功登录,LoginPartial Action 将根据 ReturnUrl 参数将用户重定向到电话详细信息。
因为 returnUrl 参数是从框架自动设置的,并且 returnUrl 将是 /Home/PhoneDetail action
这就是问题所在。 当用户重定向到 /Home/PhoneDetail 时,用户将看到 html 标签,而不是电话详细信息
所以,我在提交页面时使用 Javascript 函数添加了 returnUrl 参数。 如果在 /Home/PhoneDetail 页面上添加了 returnUrl 参数,returnUrl 参数也将被重定向。
结果,您可以看到 phonedetail 页面。
如果您想查看在没有 returnUrl 的情况下提交页面会发生什么,只需删除 Javascript 函数中的 '?returnUrl=' + encodeURIComponent(returnUrl) 这部分。

@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>

这是 RegisterPartialView 代码。 它是从 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")
}


历史

2014 年 9 月 11 日 - 初始发布

2014 年 9 月 14 日 - 损坏的演示链接更新

© . All rights reserved.