[譯]ABP框架使用AngularJs,ASP.NET MVC,Web API和EntityFramework構建N層架構的SPA應用程序


本文演示ABP框架如何使用AngularJs,ASP.NET MVC,Web API 和EntityFramework構建基於N層架構的多語言SPA應用程序

 

 

Simple Task System Screenshot
演示程序截圖如上所示.

內容摘要

介紹

在這篇文章, 我將基於以下框架演示如何開發單頁面的(SPA) 應用程序 :

  • ASP.NET MVC 和 ASP.NET Web API  web站點的基礎框架.
  • Angularjs  SPA 框架.
  • EntityFramework  ORM (Object-Relational Mapping) 框架
  • Castle Windsor – 依賴注入框架.
  • Twitter Bootstrap – 前端框架.
  • Log4Net 來記錄日志, AutoMapper 實體對象映射.
  • 和 ASP.NET Boilerplate 作為應用程序模板框架.

ASP.NET Boilerplate [1] 是一個開源的應用程序框架,它包含了一些常用的組件讓您能夠快速簡單的開發應用. 它集成一些常用的基礎框架. 比如依賴注入領域驅動設計 和分層架構. 本應用演示ABP如何實現驗證,異常處理,本地化響應式設計.

使用 boilerplate 模板創建程序

ASP.NET Boilerplate給我們提供了一個非常好的構建企業應用的模板,以便節約我們構建應用程序的時間。
在www.aspnetboilerplate.com/Templates目錄,我們可以使用模板創建應用。

 

Create template by ASP.NET Boilerplate

這里我選擇 SPA(單頁面程序)使用AngularJs EntityFramework框架. 然后輸入項目名稱SimpleTaskSystem.來創建和下載應用模版.下載的模版解決方案包含5個項目. Core 項目是領域 (業務) 層, Application 項目是應用層, WebApi 項目實現了 Web Api 控制器, Web 項目是展示層,最后EntityFramework 項目實現了EntityFramework框架.

Note: 如果您下載本文演示實例, 你會看到解決方案有7個項目. 我把NHibernate和Durandal都放到本演示中.如果你對NHibernate,Durandal不感興趣的話,可以忽略它們.

創建實體對象

我將創建一個簡單的應用程序來演示任務和分配任務的人. 所以我需要創建Task實體對象和Person實體對象.

Task實體對象簡單的定義了一些描述:CreationTime和Task的狀態. 它同樣包含了Person(AssignedPerson)的關聯引用:

 

public class Task : Entity<long> { [ForeignKey("AssignedPersonId")] public virtual Person AssignedPerson{ get; set; } public virtual int? AssignedPersonId { get; set; } public virtual string Description { get; set; } public virtual DateTime CreationTime { get; set; } public virtual TaskState State { get; set; } public Task() { CreationTime = DateTime.Now; State = TaskState.Active; } }

 

 
        

Person實體對象簡單的定義下Name:

public class Person : Entity { public virtual string Name { get; set; } }

ASP.NET Boilerplate 給 Entity 類定義了 Id 屬性. 從Entity 類派生實體對象將繼承Id屬性. Task 類從 Entity<long>派生將包含 long 類型的ID. Person 類包含 int 類型的ID. 因為int是默認的主鍵類型, 這里我不需要特殊指定.

我在這個 Core 項目里添加實體對象因為實體對象是屬於領域/業務層的.

創建 DbContext

眾所周知, EntityFramework使用DbContext 類工作. 我們首先得定義它. ASP.NET Boilerplate創建了一個DbContext模板給我們. 我們只需要添加 IDbSets給 Task and Person. 完整的 DbContext 類如下:

public class SimpleTaskSystemDbContext : AbpDbContext { public virtual IDbSet<Task> Tasks { get; set; } public virtual IDbSet<Person> People { get; set; } public SimpleTaskSystemDbContext(): base("Default") { } public SimpleTaskSystemDbContext(string nameOrConnectionString): base(nameOrConnectionString) { } }

我們還需要在web.config添加默認的連接字符串. 如下:

<add name="Default" connectionString="Server=localhost; Database=SimpleTaskSystem; Trusted_Connection=True;" providerName="System.Data.SqlClient" />

創建數據庫遷移

我們將使用EntityFramework的Code First模式來遷移和創建數據庫. ASP.NET Boilerplate模板默認支持簽約但需要我們添加如下的Configuration 類:

internalinternal sealed class Configuration : DbMigrationsConfiguration<SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext> { public Configuration() { AutomaticMigrationsEnabled = false; } protected override void Seed(SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext context) { context.People.AddOrUpdate( p => p.Name, new Person {Name = "Isaac Asimov"}, new Person {Name = "Thomas More"}, new Person {Name = "George Orwell"}, new Person {Name = "Douglas Adams"} ); } }

另一種方法, 是在初始化的是添加4個Person. 我將創建初始遷移.打開包管理控台程序並輸入以下命令:

Visual studio Package manager console

Add-Migration “InitialCreate” 命令創建 InitialCreate 類如下:

public partial class InitialCreate : DbMigration { public override void Up() { CreateTable( "dbo.StsPeople", c => new { Id = c.Int(nullable: false, identity: true), Name = c.String(), }) .PrimaryKey(t => t.Id); CreateTable( "dbo.StsTasks", c => new { Id = c.Long(nullable: false, identity: true), AssignedPersonId = c.Int(), Description = c.String(), CreationTime = c.DateTime(nullable: false), State = c.Byte(nullable: false), }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.StsPeople", t => t.AssignedPersonId) .Index(t => t.AssignedPersonId); } public override void Down() { DropForeignKey("dbo.StsTasks", "AssignedPersonId", "dbo.StsPeople"); DropIndex("dbo.StsTasks", new[] { "AssignedPersonId" }); DropTable("dbo.StsTasks"); DropTable("dbo.StsPeople"); } }

我們已經創建了數據庫類, 但是還沒創建那數據庫. 下面來創建數據庫,命令如下:

PM> Update-Database

這個命令幫我們創建好了數據庫並填充了初始數據:

Database created by EntityFramework Migrations

當我們修改實體類時, 我們可以通過 Add-Migration 命令很容易的創建遷移類。需要更新數據庫的時候則可以通過 Update-Database 命令. 關於更多的數據庫遷移, 可以查看 entity framework的官方文檔.

定義庫

在領域驅動設計中, repositories 用於實現特定的代碼. ASP.NET Boilerplate 使用 IRepository 接口給每個實體自動的創建 repository . IRepository 定義了常用的方法如 select, insert, update, delete 等,更多如下:

IRepository interface

我們還可以根據我們的需要來擴展 repository . 如果需要單獨的實現接口的話,首先需要繼承 repositories 接口. Task repository 接口如下:

public interface ITaskRepository : IRepository<Task, long> { List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state); }

這繼承了ASP.NET Boilerplate 的 IRepository 接口. ITaskRepository 默認定義了這些方法. 我們也可以添加自己的方法 GetAllWithPeople(…).

這里不需要再為Person創建 repository 了,因為默認的方法已經足夠. ASP.NET Boilerplate 提供了通用的 repositories 而不需要創建 repository 類. 在’構建應用程序服務層’ 章節中的TaskAppService 類中將演示這些..

repository 接口被定義在Core 項目中因為它們是屬於領域/業務層的.

實現庫

我們需要實現上述的 ITaskRepository 接口. 我們在EntityFramework 項目實現 repositories. 因此,領域層完全獨立於 EntityFramework.

當我們創建項目模板, ASP.NET Boilerplate 在項目中為 repositories 定義了一些基本類: SimpleTaskSystemRepositoryBase. 這是一個非常好的方式來添加基本類,因為我們可以為repositories稍后添加方法. 你可以看下面代碼定義的這個類.定義TaskRepository 從它派生:

public class TaskRepository : SimpleTaskSystemRepositoryBase<Task, long>, ITaskRepository { public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state) { //In repository methods, we do not deal with create/dispose DB connections, DbContexes and transactions. ABP handles it. var query = GetAll(); //GetAll() returns IQueryable<T>, so we can query over it. //var query = Context.Tasks.AsQueryable();  //Alternatively, we can directly use EF's DbContext object. //var query = Table.AsQueryable();  //Another alternative: We can directly use 'Table' property instead of 'Context.Tasks', they are identical. //Add some Where conditions... if (assignedPersonId.HasValue) { query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value); } if (state.HasValue) { query = query.Where(task => task.State == state); } return query .OrderByDescending(task => task.CreationTime) .Include(task => task.AssignedPerson) //Include assigned person in a single query .ToList(); } }

上述代碼 TaskRepository 派生自 SimpleTaskSystemRepositoryBase 並實現了 ITaskRepository.

構建應用程序服務層

應用程序服務層被用於分離表示層和領域層並提供一些界面的樣式方法. 在 Application 組件中定義應用程序服務. 首先定義 task 應用程序服務的接口:

public interface ITaskAppService : IApplicationService { GetTasksOutput GetTasks(GetTasksInput input); void UpdateTask(UpdateTaskInput input); void CreateTask(CreateTaskInput input); }

ITaskAppService繼承自IApplicationService. 因此ASP.NET Boilerplate自動的提供了一些類的特性(像依賴注入和驗證).現在我們來實現ITaskAppService:

public class TaskAppService : ApplicationService, ITaskAppService { //These members set in constructor using constructor injection. private readonly ITaskRepository _taskRepository; private readonly IRepository<Person> _personRepository; /// <summary>  ///In constructor, we can get needed classes/interfaces. ///They are sent here by dependency injection system automatically. /// </summary>  public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository) { _taskRepository = taskRepository; _personRepository = personRepository; } public GetTasksOutput GetTasks(GetTasksInput input) { //Called specific GetAllWithPeople method of task repository. var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State); //Used AutoMapper to automatically convert List<Task> to List<TaskDto>. return new GetTasksOutput { Tasks = Mapper.Map<List<TaskDto>>(tasks) }; } public void UpdateTask(UpdateTaskInput input) { //We can use Logger, it's defined in ApplicationService base class. Logger.Info("Updating a task for input: " + input); //Retrieving a task entity with given id using standard Get method of repositories. var task = _taskRepository.Get(input.TaskId); //Updating changed properties of the retrieved task entity. if (input.State.HasValue) { task.State = input.State.Value; } if (input.AssignedPersonId.HasValue) { task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value); } //We even do not call Update method of the repository. //Because an application service method is a 'unit of work' scope as default. //ABP automatically saves all changes when a 'unit of work' scope ends (without any exception). } public void CreateTask(CreateTaskInput input) { //We can use Logger, it's defined in ApplicationService class.  Logger.Info("Creating a task for input: " + input); //Creating a new Task entity with given input's properties var task = new Task { Description = input.Description }; if (input.AssignedPersonId.HasValue) { task.AssignedPersonId = input.AssignedPersonId.Value; } //Saving entity with standard Insert method of repositories. _taskRepository.Insert(task); } }

TaskAppService 使用倉儲來操作數據庫. 它過在構造函數中注入倉儲. ASP.NET Boilerplate實現了依賴注入, 所以我們可以自由的使用構造函數注入和屬性注入 (更多的依賴注入章節查看 ASP.NET Boilerplate 文檔).

注意:我們使用PersonRepository來注入IRepository<Person>. ASP.NET Boilerplate會自動給我們的實體創建庫. 如果默認的庫接口夠用的話,我們就不需要在重新定義實體庫類型了.

應用服務層的方法使用了Data Transfer Objects (DTOs)協議. 這是一個非常好的方式,我同樣建議大家這么做. 但是你也沒必要非這么做不可,如果你能處理你的問題並傳輸到展示層。.

GetTasks方法中,我們使用GetAllWithPeople方法,它返回List<Task>類型, 但我可能需要返回一個List<TaskDto>給展現層. 這時候AutoMapper自動的幫助我們把TaskDto對象轉換為Task對象.GetTasksInput和GetTasksOutput是特殊的DTOs被定義在GetTasks方法中.

UpdateTask方法中,我從數據庫返回Task(使用IRepository的Get方法)並更新Task屬性.注意我並沒有調用reponsitory的Update方法. ASP.NET Boilerplate實現工作單元模式. 所以,在應用服務層的所有改變都是以工作單元形式,並最后自動保存到數據庫中.

CreateTask方法中,使用IRepository的Insert方法創建新Task到數據庫.

ASP.NET Boilerplate的ApplicationService類提供一些屬性來簡化開發應用服務.例如,它定義了Logger給日志. 你也可以自己實現,但要繼承IApplicationService接口(注意:ITaskAppService繼承自IApplicationService).

驗證

ASP.NET Boilerplate在服務層方法輸入的參數. CreateTask method getsCreateTaskInput as parameter:

public class CreateTaskInput { public int? AssignedPersonId { get; set; } [Required] public string Description { get; set; } }

這里的Description標記是必須輸入的意思. 這里你可以使用任何的Data Annotation屬性. 如果你需要使用自定義屬性, 你可以繼承ICustomValidate 接口來實現UpdateTaskInput:

public class UpdateTaskInput : ICustomValidate { [Range(1, long.MaxValue)] public long TaskId { get; set; } public int? AssignedPersonId { get; set; } public TaskState? State { get; set; } public void AddValidationErrors(List<ValidationResult> results) { if (AssignedPersonId == null && State == null) { results.Add(new ValidationResult("Both of AssignedPersonId and State can not be null in order to update a Task!", new[] { "AssignedPersonId", "State" })); } } public override string ToString() { return string.Format("[UpdateTask > TaskId = {0}, AssignedPersonId = {1}, State = {2}]", TaskId, AssignedPersonId, State); } }

你可以替換AddValidationErrors方法里面的內容來自定義錯誤代碼.

處理異常

注意我們沒有做任何的異常處理. ASP.NET Boilerplate 自動的處理了異常, 日志和返回友好的錯誤信息給客戶端. 客戶端僅需處理錯誤信息並顯示給用戶.  異常處理文檔查看.

構建Web API服務

我把應用服務層暴露給遠程客戶端,以便AngularJs能夠簡單的使用AJAX調用.

ASP.NET Boilerplate使用自動的方法來提供應該程序服務,即ASP.NET Web API.如DynamicApiControllerBuilder:

DynamicApiControllerBuilder
    .ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem") .Build();

在本段代碼中, ASP.NET Boilerplate找所有繼承自IApplicationService接口的方法並創建web api controller給每個應用程序服務類. 這里還有幾種其他的查找control的方法.我們將在如何使用AJAX調用服務中看到.

開發SPA

我要在項目中實現一個單頁面的web應用程序. AngularJs(Google開發的)是一個最流行的最火的SPA框架.

ASP.NET Boilerplate提供了一個模版來簡單的使用AngularJs.這個模版有兩個頁面(Home 和About)能平滑過度. 使用了Twitter的Bootstrap前端框架.ASP.NET Boilerplate它默認定義了English和Turkish兩種本地語言(你可以簡單的添加刪除語言).

我們先改變路由模版. ASP.NET Boilerplate模版使用AngularUI-Router, 這實際上是標准的AngularJs路由.它提供了路由狀態模式. 我們有兩個views: task list 和 new task. 所以我們將在app.js中來做如下的定義:

app.config([ '$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { $urlRouterProvider.otherwise('/'); $stateProvider .state('tasklist', { url: '/', templateUrl: '/App/Main/views/task/list.cshtml', menu: 'TaskList' //Matches to name of 'TaskList' menu in SimpleTaskSystemNavigationProvider }) .state('newtask', { url: '/new', templateUrl: '/App/Main/views/task/new.cshtml', menu: 'NewTask' //Matches to name of 'NewTask' menu in SimpleTaskSystemNavigationProvider }); } ]);

app.js是javascript的入口文件,用來配置SPA的啟動. 注意這里使用的是cshtml示圖文件! 一般情況下, AngularJs的顯示頁面是html. 而ASP.NET Boilerplate使用cshtml文件. 因此強大的razor引擎將會把cshtml生成HTML.

ASP.NET Boilerplate提供了基礎框架來創建和顯示菜單.可以用c#或javascript語言來定義菜單. SimpleTaskSystemNavigationProvider類來創建菜單 ,header.js/header.cshtml用來顯示菜單.

首先我們創建一個Angular controllertask列表頁面:

(function() { var app = angular.module('app'); var controllerId = 'sts.views.task.list'; app.controller(controllerId, [ '$scope', 'abp.services.tasksystem.task', function($scope, taskService) { var vm = this; vm.localize = abp.localization.getSource('SimpleTaskSystem'); vm.tasks = []; $scope.selectedTaskState = 0; $scope.$watch('selectedTaskState', function(value) { vm.refreshTasks(); }); vm.refreshTasks = function() { abp.ui.setBusy( //Set whole page busy until getTasks complete null, taskService.getTasks({ //Call application service method directly from javascript state: $scope.selectedTaskState > 0 ? $scope.selectedTaskState : null }).success(function(data) { vm.tasks = data.tasks; }) ); }; vm.changeTaskState = function(task) { var newState; if (task.state == 1) { newState = 2; //Completed } else { newState = 1; //Active } taskService.updateTask({ taskId: task.id, state: newState }).success(function() { task.state = newState; abp.notify.info(vm.localize('TaskUpdatedMessage')); }); }; vm.getTaskCountText = function() { return abp.utils.formatString(vm.localize('Xtasks'), vm.tasks.length); }; } ]); })();

我定義了名為’sts.views.task.list‘的控制器. 這是我的命名習慣(for scalable code-base)但你也可以簡單的命名為’ListController’. AngularJs也使用依賴注入. 我們這里注入’$scope‘和’abp.services.tasksystem.task‘.首先是Angular的scope變量再次是ITaskAppService自動創建的的javascript服務代理(我們在’Build Web API services’之前就創建了).

ASP.NET Boilerplate 提供了基礎設施本地語言文件用於服務端和客戶端. 

vm.taks是頁面的任務列表.vm.refreshTasks方法內執行了taskService獲取task的數據集合.這是在selectedTaskState修改的時候被調用(查看執行使用$scope.$watch).

正如你所看到的,調用應用服務的方法是如此的簡單!這就是ASP.NET Boilerplate的特性.它生成了Web API層和Javascript代理層.因此我們調用應用服務層像調用javascript方法一樣. 它完全集成進了AngularJs (使用Angular的$http service).

我們來看看任務列表的頁面:

<div class="panel panel-default" ng-controller="sts.views.task.list as vm"> <div class="panel-heading" style="position: relative;"> <div class="row"> <!-- Title --> <h3 class="panel-title col-xs-6"> @L("TaskList") - <span>{{vm.getTaskCountText()}}</span> </h3> <!-- Task state combobox --> <div class="col-xs-6 text-right"> <select ng-model="selectedTaskState"> <option value="0">@L("AllTasks")</option> <option value="1">@L("ActiveTasks")</option> <option value="2">@L("CompletedTasks")</option> </select> </div> </div> </div> <!-- Task list --> <ul class="list-group" ng-repeat="task in vm.tasks"> <div class="list-group-item"> <span class="task-state-icon glyphicon" ng-click="vm.changeTaskState(task)" ng-class="{'glyphicon-minus': task.state == 1, 'glyphicon-ok': task.state == 2}"></span> <span ng-class="{'task-description-active': task.state == 1, 'task-description-completed': task.state == 2 }">{{task.description}}</span> <br /> <span ng-show="task.assignedPersonId > 0"><span class="task-assignedto">{{task.assignedPersonName}}</span> </span> <span class="task-creationtime">{{task.creationTime}}</span> </div> </ul> </div>

ng-controller 屬性(在第一行) 綁定頁面的controller. @L(“TaskList”) 獲取”task list”的本地語言文本(服務器端解析html的時候執行). 因為這是個cshtml文件.

ng-model 綁定combobox和javascript變量. 當變量值改變combobox就會被更新.當改變combobox變量就會被更新. 這是AngularJs的雙向綁定.

ng-repeat 是Angular的另一個指令用於循環集合里面的值. 當集合改變(例如增加值),它會自動更新界面. 這是AngularJs的另一個強大特性.

注意: 當你應該在頁面添加javascript文件 (例如, 添加’task list’控制器)時.可以改成在添加Home\Index.cshtml模版時添加.

本地化

ASP.NET Boilerplate提供了靈活健壯的本地化系統.你可以使用XML文件或者資源文件做為本地化的數據源.你也可以自定義數據源.更多信息可以查看文檔. 本示例使用XML文件演示(在web應用項目的Localization文件夾里):

<?xml version="1.0" encoding="utf-8" ?> <localizationDictionary culture="en"> <texts> <text name="TaskSystem" value="Task System" /> <text name="TaskList" value="Task List" /> <text name="NewTask" value="New Task" /> <text name="Xtasks" value="{0} tasks" /> <text name="AllTasks" value="All tasks" /> <text name="ActiveTasks" value="Active tasks" /> <text name="CompletedTasks" value="Completed tasks" /> <text name="TaskDescription" value="Task description" /> <text name="EnterDescriptionHere" value="Task description" /> <text name="AssignTo" value="Assign to" /> <text name="SelectPerson" value="Select person" /> <text name="CreateTheTask" value="Create the task" /> <text name="TaskUpdatedMessage" value="Task has been successfully updated." /> <text name="TaskCreatedMessage" value="Task {0} has been created successfully." /> </texts> </localizationDictionary>

使用單元測試

ASP.NET Boilerplate 是可測試的. 我的另一篇文章展示了如何使用ABP 基本項目集成集成單元測試. 查看文章: Unit testing in C# using xUnit, Entity Framework, Effort and ASP.NET Boilerplate.

摘要

在這篇文章, 我闡述了如何在 ASP.NET MVC web 應用中開發N層架構的SPA應用. ASP.NET Boilerplate 使用非常好的方式並且如此簡單的創建了應用. 下面的鏈接可以獲得更多信息:

文章歷史

  • 2016-10-26: Upgraded sample project to ABP v1.0.
  • 2016-07-19: Updated article and sample project for ABP v0.10.
  • 2015-06-08: Updated article and sample project for ABP v0.6.3.1.
  • 2015-02-20: Added link to unit test article and updated the sample project
  • 2015-01-05: Updated sample project for ABP v0.5.
  • 2014-11-03: Updated article and sample project for ABP v0.4.1.
  • 2014-09-08: Updated article and sample project for ABP v0.3.2.
  • 2014-08-17: Updated sample project to ABP v0.3.1.2.
  • 2014-07-22: Updated sample project to ABP v0.3.0.1.
  • 2014-07-11: Added screenshot of ‘Enable-Migrations’ command.
  • 2014-07-08: Updated sample project and article.
  • 2014-07-01: First publish of the article.

引用

[1] ASP.NET Boilerplate 官網: http://www.aspnetboilerplate.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM