- 下载sample application (or see on Github)
内容 问题介绍什么是ASP。NET样板文件NET Boilerplate不是开始创建空的web应用程序从模板域层 关于名称空间的实体存储库 基础设施层 数据库迁移、实体映射存储库实现 应用程序层 应用服务和数据传输对象(dto)验证动态Web API控制器 表示层 单页面应用程序视图和视图模型 任务列表新任务 本地化的javascript API 更多的 模块系统依赖注入和约定 关于Angularjs,EntityFramework示例摘要文章历史引用 问题简介 重要提示: 虽然这篇文章显示开发用户界面vith Durandal,它不是 可以创建Durdandal模板 http://aspnetboilerplate.com 了。但是这篇文章对那些想要使用NHibernate作为ORM的人还是很有用的 在基地组织的框架内,了解基地组织的一般情况。点击 这里是Angular &本文的EntityFramework版本。 干——不要重复!是一个好的开发人员在开发软件时的主要思想之一。我们尝试从简单的方法到类和模块实现它。开发一个新的基于web的应用程序怎么样?我们作为软件开发人员,在开发企业web应用程序时也有类似的需求。 企业web应用程序需要登录页面、用户/角色管理基础设施、用户/应用程序设置管理、本地化等。此外,高质量和大规模的软件实现了诸如分层架构、领域驱动设计(DDD)、依赖注入(DI)等最佳实践。此外,我们还使用了对象关系映射(ORM)、数据库迁移、日志记录等工具。等。当涉及到用户界面(UI)时,它没有太大的不同。 启动一个新的企业web应用程序是一项艰巨的工作。因为所有的应用程序都需要一些常见的任务,所以我们在重复。许多公司正在为此类常见任务开发自己的应用程序框架或库,以避免重新开发相同的东西。另一些则复制现有应用程序的某些部分,并为其新应用程序准备一个起点。如果您的公司足够大并且有时间开发这样一个框架,那么第一种方法是非常好的。 作为一名软件架构师,我也为自己的公司开发了这样一个框架。但是,有一点让我感觉很糟糕:许多公司重复同样的任务。如果我们能分享得更多,重复得更少呢?如果DRY原则被普遍执行,而不是每个项目或每个公司执行,会怎么样?这听起来很乌托邦,但我认为可能有一个起点! 什么是ASP。净样板吗? ASP。NET Boilerplate[1]是使用最佳实践和最流行工具的新型现代web应用程序的起点。它的目标是成为一个坚实的模型、一个通用的应用程序框架和一个项目模板。的是什么呢? 服务器端 基于最新的ASP。NET MVC和Web API。实现领域驱动设计(实体,存储库,领域服务,应用服务,dto, Unif的工作…实现分层架构(域层、应用程序层、表示层和基础设施层)。为大型项目开发可重用和可组合的模块提供基础设施。使用最流行的框架/库作为(可能)您已经使用。提供基础设施,使依赖注入易于使用(使用Castle Windsor作为DI容器)。提供了一个严格的模型和基类来轻松使用对象关系映射(直接支持EntityFramework和NHibernate)。支持并实现数据库迁移。包括一个简单灵活的定位系统。包括用于服务器端全局域事件的EventBus。管理异常处理和验证。为应用程序服务创建动态Web API层。提供基类和助手类来实现一些常见任务。使用约定优于配置原则。 客户端 为单页应用程序(使用AngularJs和Durandaljs)和多页应用程序提供项目模板。模板是基于Twitter引导的。大多数使用的javascript库都包含默认配置的ana。创建动态javascript代理来轻松调用应用程序服务(使用动态Web API层)。包括一些独特的api,为一些任务:显示警告&;通知,阻塞UI,发出AJAX请求… 除了这些公共基础设施之外,还正在开发一个名为zero的模块。它将提供一个基于角色和权限的授权系统。NET标识框架),设置系统,多租户等等。 ASP。NET Boilerplate不是? ASP。NET Boilerplate提供了一个具有最佳实践的应用程序开发模型。它有基类、接口和工具,这些使构建可维护的大型文件变得容易凯尔的应用程序。但. . 它不是RAD(快速应用程序开发)工具中的一种,这些工具试图提供不需要编码就能构建应用程序的基础设施。相反,它提供了在最佳实践中编写代码的基础设施。它不是一个代码生成工具。虽然它有几个在运行时构建动态代码的特性,但它并不生成代码。它不是一个集所有功能于一身的框架。相反,它使用众所周知的工具/库来完成特定的任务(比如NHibernate和EntityFramework用于O/RM, Log4Net用于日志记录,Castle Windsor作为DI容器,AngularJs用于SPA框架)。 开始 在本文中,我将展示如何使用ASP删除一个单页面响应式Web应用程序。NET Boilerplate(从现在起我将称之为ABP)。这里我将使用DurandalJs作为SPA框架,NHibernate作为ORM框架。我准备了另一篇文章,用AngularJs和EntityFramework实现同样的应用程序。 这个示例应用程序名为“Simple Task System”,它由两个页面组成:一个用于任务列表,另一个用于添加新任务。一项任务可以与一个人相关,可以是活动的,也可以是已完成的。应用程序被本地化为两种语言。应用程序中的任务列表截图如下: 从模板创建空的web应用程序 ABP为新项目提供了启动模板(即使您可以手动创建项目并从nuget获得ABP包,模板方式也要简单得多)。访问www.aspnetboilerplate.com/Templates从模板创建应用程序。您可以选择SPA(单页应用程序)项目与可选的AngularJs或DurandalJs。或者您可以选择MPA(经典的、多页面应用程序)项目。然后你可以选择EntityFramework或NHibernate作为ORM框架。 我将我的项目命名为SimpleTaskSystem,并用Durandal和NHibernate创建了一个SPA项目。下载的项目作为一个zip文件。当我打开zip文件,我看到一个解决方案准备好了,包含程序集(项目)的每层领域驱动设计: 创建项目的运行时是。net Framework 4.5.1,我建议用Visual Studio 2013打开。能够运行项目的唯一前提是创建数据库。SPA模板假设您使用的是SQL Server 2008或更高版本。但是你可以很容易地把它改成另一个DBMS。请参阅web中的连接字符串。web项目的配置文件: 隐藏,复制Code
<addname="Default"connectionString="Server=localhost; Database=SimpleTaskSystemDb; Trusted_Connection=True;"/>
您可以在这里更改连接字符串。我没有改变数据库名称,所以我创建了一个空数据库,命名为SimpleTaskSystemDb,在SQL Server: 这样,您的项目就可以运行了!在VS2013中打开,按F5: 模板由两个页面组成:一个是主页,另一个是关于页面。它的本地化是英语和土耳其语。它是单页应用程序!尝试在页面之间导航,您将看到只有内容在变化,导航菜单是固定的,所有脚本和样式只加载一次。响应。尝试改变浏览器的大小。 现在,我将逐层演示如何将应用程序更改为一个简单的任务系统应用程序。 领域层 “负责表示业务的概念、关于业务情况的信息和业务规则”(Eric Evans)[2]。在领域驱动设计(DDD)中,核心层是领域层。域层定义了实体,实现了业务规则,等等。 实体 实体是DDD的核心概念之一。埃里克·埃文斯(Eric Evans)将其描述为“一个从根本上不是由其属性来定义的物体,而是由连续性和同一性的线索来定义的”。实体有Id并存储在数据库中。 我的第一个实体是任务: 隐藏,复制Code
public class Task : Entity<long>
{
public virtual Person AssignedPerson { 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;
}
}
它是从具有长主键类型的实体基类派生的简单类。TaskState是一个枚举,它有“活动的”和“完成的”。第二个实体是人: 隐藏,复制Code
public class Person : Entity
{
public virtual string Name { get; set; }
}
任务与人有关系,这就是这个简单应用程序的全部内容。 实体实现IEntity< TPrimaryKey>因此,如果你的主键的类型对于一个实体来说很长,它必须实现IEntity< >如果您的实体的主键是int,您可以不定义主键类型并直接实现IEntity接口。在实践中,您可以很容易地从Entity或entitys&primarykey>类如上所示(Task和Person)。IEntity为实体定义了Id属性。 存储库 “使用类似于集合的接口来访问域对象,在域和数据映射层之间进行中介”(Martin Fowler)[3]。在实践中,存储库用于执行域对象(实体或值类型)的数据库操作。 通常,每个实体(或聚合根)使用独立的存储库。 ASP。NET Boilerplate为每个实体提供了默认存储库(我们将看到 如何使用默认存储库)。如果我们需要 定义额外的方法,我们可以扩展IRepository接口。我把它延长到任务 存储库: 隐藏,复制Code
public interface ITaskRepository : IRepository<Task, long>
{
List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);
}
最好为每个存储库定义一个接口。因此,我们可以将接口与实现分离。 IRepository接口定义了存储库最常见的方法: 它定义了基本的CRUD方法。所有的存储库都会自动实现所有这些方法。除了标准的基础方法之外,您还可以添加特定于这个存储库的方法,如我定义的GetAllWithPeople方法。 关于名称空间 当您研究样例应用程序的源代码时,您将看到按意图和域打包的类,相关的类/接口/枚举在同一个名称空间中,而不是按基础结构打包。我会把Task类放到TaskSystem中。实体名称空间,ITaskRepository到任务系统。存储库名称空间,任务状态到任务系统。枚举名称空间等等…相反,我将所有这些类放在TTaskSystem中。任务名称空间,因为所有这些都是相互关联的。这更适合领域驱动设计的本质。所以,我认为把所有实体都放到TaskSystem中并不是一个好的做法。实体名称空间。你们有些人可能不会这么想。我可以理解他们,因为我一直这样做,直到不久以前。但我确实看到了问题,我强烈建议将相关的类/接口/枚举放置到同一个名称空间,可能是不同的程序集,但使用同一个名称空间。您可以阅读Eric Evans的《领域驱动设计[2]》一书中的“基础结构驱动打包的缺陷”一节。 基础设施层 “提供支持较高层的通用技术能力”(Eric Evans)。它用于使用第三方库实现应用程序的抽象。框架,如对象-关系映射。在这个应用程序中,我将使用基础设施层: 使用FluentMigrator创建数据库迁移系统。使用NHibernate和FluentNHibernate实现存储库和映射实体。 数据库迁移 进化式数据库设计:在过去的几年里,我们已经开发了许多技术,允许数据库设计随着应用程序的发展而进化。这对于敏捷方法来说是一个非常重要的能力。”马丁·福勒在他的网站[3]中说。数据库迁移是支持这一思想的一项重要技术。如果没有这样的技术,就很难在多个生产环境中维护应用程序的数据库。即使您只有一个活动的系统,这也是至关重要的。 FluentMigrator[4]是一个用于数据库迁移的好工具。它支持大多数常见的数据库系统。这里是我的个人表和任务表的迁移代码。 隐藏,收缩,复制Code
[Migration(2014041001)]
public class _01_CreatePersonTable : AutoReversingMigration
{
public override void Up()
{
Create.Table("StsPeople")
.WithColumn("Id").AsInt32().Identity().PrimaryKey().NotNullable()
.WithColumn("Name").AsString(32).NotNullable(); Insert.IntoTable("StsPeople")
.Row(new { Name = "Douglas Adams" })
.Row(new { Name = "Isaac Asimov" })
.Row(new { Name = "George Orwell" })
.Row(new { Name = "Thomas More" });
}
} [Migration(2014041002)]
public class _02_CreateTasksTable : AutoReversingMigration
{
public override void Up()
{
Create.Table("StsTasks")
.WithColumn("Id").AsInt64().Identity().PrimaryKey().NotNullable()
.WithColumn("AssignedPersonId").AsInt32().ForeignKey("TsPeople", "Id").Nullable()
.WithColumn("Description").AsString(256).NotNullable()
.WithColumn("State").AsByte().NotNullable().WithDefaultValue(1) //1: TaskState.New
.WithColumn("CreationTime").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime);
}
}
在FluentMigrator中,在派生自迁移的类中定义迁移。如果迁移可以自动回滚,那么自动逆转迁移是一种快捷方式。一个迁移类应该具有MigrationAttribute。它定义了迁移类的版本号。所有迁移都是按这个版本号排序的。它可以是任意长的数字。我使用一个数字来标识迁移类的创建日期和同一天的增量值(例如:对于2014年4月24日的第二个迁移类,版本是'2014042402')。完全由你决定。唯一重要的是它们的相对顺序。 FluentMigrator将最新应用的版本号存储在数据库中的一个表中。因此,它只应用那些比数据库版本大的迁移。默认情况下,它使用'VersionInfo'表。如果你想改变表名,你可以创建这样一个类: 隐藏,复制Code
[VersionTableMetaData]
public class VersionTable : DefaultVersionTableMetaData
{
public override string TableName
{
get
{
return "StsVersionInfo";
}
}
}
如您所见,我为所有表编写了一个前缀Sts (Simple Task System)。这对于模块化应用程序非常重要,这样所有模块都可以使用它们特定的前缀来标识特定于模块的表。 为了在数据库中创建我的表,我使用FluentMigrator的migration .exe工具和这样的“命令行”命令: 隐藏,复制Code
Migrate.exe /connection "Server=localhost; Database=SimpleTaskSystemDb; Trusted_Connection=True;" /db sqlserver /target "SimpleTaskSystem.Infrastructure.NHibernate.dll"
作为快捷方式,ABP模板包括runmigration .bat文件。在调试模式下编译项目后,我运行“runmigration .bat”: 如您所见,执行了两个迁移文件并创建了表: 有关FluentMigrator的更多信息,请参见它的网站[4]。 实体映射 为了将实体获取/存储到数据库中,我们应该将实体与数据库表进行映射。NHibernate有几个选项来完成这个任务。在这里,我将使用手动流畅映射(你可以使用常规的自动映射,见FluentNHibernate的网站[5]): 隐藏,复制Code
public class PersonMap : EntityMap<Person>
{
public PersonMap()
: base("StsPeople")
{
Map(x => x.Name);
}
} public class TaskMap : EntityMap<Task, long>
{
public TaskMap()
: base("StsTasks")
{
Map(x => x.Description);
Map(x => x.CreationTime);
Map(x => x.State).CustomType<TaskState>();
References(x => x.AssignedPerson).Column("AssignedPersonId").LazyLoad();
}
}
EntityMap是一个ABP类,它在构造函数中自动映射Id属性并获取表名。我从它衍生并映射其他属性。 库的实现 我在域层为任务存储库(ITaskRepository)定义了接口。在这里,我将实现 在NHibernate中: 隐藏,收缩,复制Code
public class TaskRepository : NhRepositoryBase<Task, long>, ITaskRepository
{
public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state)
{
//In repository methods, we do not deal with create/dispose DB connections (Session) and transactions. ABP handles it. var query = GetAll(); //GetAll() returns IQueryable<T>, so we can query over it.
//var query = Session.Query<Task>(); //Alternatively, we can directly use NHibernate's Session //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)
.Fetch(task => task.AssignedPerson) //Fetch assigned person in a single query
.ToList();
}
}
NhRepositoryBase实现了在IRepository接口中定义的所有方法。因此,您必须只实现您的自定义方法,就像我为GetAllWithPeople。 GetAll()方法返回IQueryable<TEntity>,因此您可以编写额外的条件,直到调用ToList()。 如果标准,则不需要为实体定义或实现存储库 存储库方法对于这个实体来说已经足够了。所以,我没有实施 Person实体的存储库。 应用程序层 “定义软件应该做的工作,并指导表达领域对象解决问题”(埃里克·埃文斯)。应用层在理想的应用程序中不包含领域信息和业务规则(这在现实生活中可能不可能,但我们应该尽量减少)。它在表示层和域层之间进行协调。 应用服务和数据传输对象(dto) 应用程序服务提供应用层的功能。应用程序服务方法获取作为参数的数据传输对象并返回数据传输对象。直接返回实体(或其他域对象)有很多问题(比如数据隐藏、序列化和延迟加载问题)。我强烈建议不要从应用程序服务中获取/返回实体或任何其他域对象。他们应该只返回dto。因此,表示层与域层是完全隔离的。 所以,让我们从简单的一个开始,个人应用服务: 隐藏,复制Code
public interface IPersonAppService : IApplicationService
{
GetAllPeopleOutput GetAllPeople();
}
所有应用程序服务都按照约定实现IApplicationService。它确保了依赖注入,并提供了一些ABP的内置特性(比如验证, 审核日志记录和授权)。我只定义了一个名为GetAllPeople()的方法,并返回一个名为GetAllPeople的DTO输出。我这样命名dto:方法名加上输入或输出后缀。看到GetAllPeopleOutput类: 隐藏,复制Code
public class GetAllPeopleOutput
{
public List<PersonDto> People { get; set; }
}
PersonDto是另一个将Person信息传递给表示层的DTO类: 隐藏,复制Code
[AutoMapFrom(typeof(Person))] //AutoMapFrom attribute maps Person -> PersonDto
public class PersonDto : EntityDto
{
public string Name { get; set; }
}
EntityDto是ABP的另一个帮助类,它定义了Id属性。 属性创建从Person到的自动映射配置 AutoMapper PersonDto。IPersonAppService的实现如下: 隐藏,复制Code
public class PersonAppService : IPersonAppService //Optionally, you can derive from ApplicationService as we did for TaskAppService class.
{
private readonly IRepository<Person> _personRepository; //ABP provides that we can directly inject IRepository<Person> (without creating any repository class)
public PersonAppService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
} public GetAllPeopleOutput GetAllPeople()
{
var people = await _personRepository.GetAllListAsync();
return new GetAllPeopleOutput
{
People = people.MapTo<List<PersonDto>>()
};
}
}
PersonAppService IRepository< Person>作为构造函数中的参数。ABP的内置依赖注入系统使用Castle Windsor来处理它。所有存储库和应用程序服务都自动作为临时对象注册到IOC(控制反转)容器。所以,你没有考虑DI细节。 此外,ABP可以为实体创建标准存储库,而不需要定义或 实现存储库。 GetAllPeople()方法从数据库中获取所有人员的列表(使用ABP的开箱即用实现),并使用AutoMapper[6]库将其转换为PersonDto对象的列表。AutoMapper使用约定(以及必要的配置)使一个类映射到另一个类变得非常容易。 ABP的映射扩展方法在内部使用自动程序进行映射扩展。 隐藏,复制Code
Mapper.CreateMap<Person, PersonDto>();
想了解更多关于AutoMapper的信息,请访问它的网站[6]。其他应用服务为TaskAppService,实现如下: 隐藏,收缩,复制Code
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);
}
}
在UpdateTask方法中,我从任务存储库中获取任务实体并设置更改的属性。状态或/和被分配的personid可能被更改。注意,我没有调用_taskRepository。更新或保存更改到数据库的任何其他方法。因为,应用程序服务方法是ASP中默认的工作单元。净样板。对于一个工作单元方法,它基本上是在方法开始时打开一个数据库连接并开始事务,然后在方法结束时自动将所有更改(提交事务)保存到数据库。如果在方法执行过程中抛出异常,则回滚事务。如果一个工作单元方法调用另一个工作单元方法,它们使用相同的事务。首先调用工作单元方法自动处理连接和事务管理。 了解更多关于ASP中的工作单元系统。NET样板文件,请参阅文档。 DTO验证 验证是一个重要而关键的概念,但有点乏味 应用程序开发。ABP提供了使验证更容易的基础设施 和更好的。验证用户输入是一项应用层任务。一个 应用程序服务方法应该验证输入并抛出异常(如果给定) 输入无效。ASP。NET MVC和Web API有内置的验证系统 可以使用数据注释(如Required)实现。但应用程序 service是一个普通类,不是从Controller派生的。幸运的是,ABP提供了 类似于普通应用程序服务方法的机制(使用Castle Dynamic) 代理和拦截): 隐藏,复制Code
public class CreateTaskInput
{
public int? AssignedPersonId { get; set; } [Required]
public string Description { get; set; }
}
在这个输入DTO中,只需要Description属性。ABP在调用应用程序服务方法之前会自动检查它,如果它是空的或空的,则会抛出异常。Sy中的所有验证属性stem.ComponentModel。这里可以使用DataAnnotations名称空间。如果这些标准属性对您来说还不够,您可以实现ICustomValidate: 隐藏,复制Code
public class CreateTaskInput : IInputDto, ICustomValidate
{
public int? AssignedPersonId { get; set; } public bool SendEmailToAssignedPerson { get; set; } [Required]
public string Description { get; set; } public void AddValidationErrors(List<ValidationResult> results)
{
if (SendEmailToAssignedPerson && (!AssignedPersonId.HasValue || AssignedPersonId.Value <= 0))
{
results.Add(new ValidationResult("AssignedPersonId must be set if SendEmailToAssignedPerson is true!"));
}
}
}
还有一件事:ABP检查服务方法的输入参数是否为空。所以,你不需要为此写保护子句。 我建议为每个应用程序服务方法创建独立的输入和输出类,即使它只有一个输入参数。当通过向此方法添加其他参数来扩展应用程序时,这是很好的。它提供了在不破坏现有客户机的情况下向应用程序服务方法添加参数的方法。 动态Web API控制器 应用程序服务由表示层使用。在单页应用程序中,所有数据都是在javascript和服务器之间使用AJAX发送/接收的。ABP极大地简化了从javascript调用应用程序服务方法。它是怎么做到的呢?让我解释一下…… 应用程序服务不能被javascript直接调用。我们可以使用ASP。NET Web API来向客户端公开服务(有许多其他的框架,如Web服务、WCF、SignalR等等)。所以,可能会有这样一个流程: Javascript通过AJAX调用Web API控制器的操作,然后Web API控制器的操作调用相应的应用程序服务的方法,获取结果并返回给客户端。这很机械。ABP将此操作自动化,并可以动态地为应用程序服务创建Web API控制器。这里的所有代码,创建Web API控制器为我的应用服务:任务服务和人员服务: 隐藏,复制Code
DynamicApiControllerBuilder
.ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem")
.Build();
因此,任务和人员应用程序服务的所有方法都使用ASP向客户端公开。ABP的流畅动态控制器创建API支持对Web API隐藏方法 或者选择特定的应用程序服务,自己试试)。在表示层部分,我们将看到如何使用ABP的动态javascript代理调用这些Web API控制器。 表示层 ,“负责向用户显示信息并解释用户的命令”(Eric Evans)。DDD最明显的层是表示层,因为我们可以看到它,我们可以点击它:)。 单页的应用程序 维基百科是这样描述SPA的: 单页应用程序(SPA),也称为单页界面(SPI),是一种适合于单个web页面的web应用程序或web站点,其目标是提供类似于桌面应用程序的更流畅的用户体验。 在SPA中,所有必需的代码(HTML、JavaScript和CSS)在加载单个页面时被检索,或者根据需要动态地加载适当的资源并添加到页面中,通常是为了响应用户的操作。虽然现代web技术(如HTML5中包含的那些技术)可以为应用程序中单独的逻辑页面提供感知和导航能力,但页面在过程中的任何时候都不会重新加载,也不会控制转移到另一个页面。与单页面应用程序的交互通常涉及到与后台web服务器的动态通信。 有许多框架和库提供了构建spa的基础设施。ASP。NET Boilerplate可以与任何SPA框架一起工作,但是提供了简单的基础设施,以便更容易地与DurandalJs和AngularJs一起工作(参见用AngularJs开发的相同应用程序)。 Durandal[7]就是其中一个框架,我认为它是一个非常成功的开源项目。它建立在成功的和常用的项目上:jQuery(用于DOM操作和AJAX)、knockout.js(用于MVVM,将javascript模型与HTML绑定)和requir .js(用于管理javascript依赖和从服务器动态加载javascript)。更多信息和丰富的文档,请访问杜兰达尔的网站。 视图和视图模型 在Durandal中,页面的一部分由视图和视图模型组成。 在ABP的启动模板中,有三个视图:layout, home和about。布局为页面提供了菜单和容器。home和about是动态加载到页面中的视图。为了创建简单的任务系统应用程序,我更改了视图和模型。改变视图后,这里是所有视图和视图模型的文件: , 我有一个布局和两个视图:任务列表和新任务。让我们开始研究视图。 任务列表 在任务列表中,有一个带有任务描述、分配的人员和创建日期的所有任务列表。有一个组合框来过滤所有/avtice/已完成的任务。已完成任务显示为灰色(带有OK图标),活动任务显示为粗体(带有减号图标)。惟一完成的任务是分配给我的“完成Codeproject文章!”因此,你可以阅读这篇文章:) 让我从视图模型开始。ViewModel用于与服务器通信以执行用户操作(列出任务、更改combobox、单击图标以更改任务的状态),并提供一个模型以在视图中显示。 隐藏,年代hrink,复制Code
define(['service!tasksystem/task'],
function (taskService) { return function () {
var that = this; //an alias of this that.tasks = ko.mapping.fromJS([]); //list of tasks that.localize = abp.localization.getSource('SimpleTaskSystem'); that.selectedTaskState = ko.observable(0); //'All tasks' option is selected in combobox as default that.activate = function () {
that.refreshTasks();
}; that.refreshTasks = function () {
abp.ui.setBusy( //Set whole page busy until getTasks complete
null,
taskService.getTasks({
state: that.selectedTaskState() > 0 ? that.selectedTaskState() : null
}).done(function(data) {
ko.mapping.fromJS(data.tasks, that.tasks);
})
);
}; that.changeTaskState = function (task) {
var newState;
if (task.state() == 1) {
newState = 2;
} else {
newState = 1;
} taskService.updateTask({
taskId: task.id(),
state: newState
}).done(function () {
task.state(newState);
abp.notify.info(that.localize('TaskUpdatedMessage'));
});
};
};
});
第一个电话需要。js的define函数注册模块并声明依赖关系。依赖关系通常是另一个模块(甚至可能是视图模型)。的服务!tasksystem/task'是ABP的一种特殊语法,它引用了用于任务应用程序服务的动态Web API控制器(还记得我是如何在动态Web API控制器部分定义任务服务的吗)。gettask函数是由ABP动态创建的。 定义函数的第二个参数是模块本身。它应该是一个定义模块的函数。它的参数是自动填补Durandal使用您的依赖列表。 那tasks是一个敲除可观察数组。它是通过敲除产生的。映射函数。那本地化是一个用于本地化的函数。在javascript中动态地本地化文本是ABP的一个特性(将在本地化一节中详细介绍)。那selectedTaskState是绑定到组合框的可观察对象,用于显示所有/活动/已完成的任务。 激活是杜兰达尔的一项特殊功能。当这个视图被激活时,Durandal会自动调用这个函数。因此,我们可以编写一些代码在用户进入视图时运行。 在refreshTasks方法中,我调用了任务应用程序服务的gettask方法来从服务器加载任务。通过ABP的动态Web API控制器和动态javascript客户端代理,可以很容易地从javascript调用应用程序服务。gettask函数获得与taskappservice . gettask相同的参数。该函数将返回一个jQuery承诺,因此您可以编写一个done处理程序来获取任务应用程序服务的gettask方法的返回值。taskService。gettask还处理错误,并在需要时向用户显示错误消息。如果调用了done处理程序,则可以确保没有错误。在done处理程序中,我将检索的任务添加到那个中。任务可观测的数组。 changeTaskState也非常类似。它用于将任务的状态从活动更改为已完成或相反。在done处理程序中,您可以看到本地化和通知api的用法。 就像ViewModel是一个javascript文件一样,视图也是一个HTML文件。在这个HTML文件中,您可以将ViewModel绑定到HTML元素。ASP。NET样板文件通过允许将视图定义为Razor视图而将这一功能向前推进了一步:使用动态cshtml文件来代替Razor视图。静态html文件。因此,您可以编写c#代码来创建视图。查看任务列表视图: 隐藏,收缩,复制Code
<divclass="panel panel-default">
<divclass="panel-heading"style="position: relative;">
<divclass="row">
<h3class="panel-title col-xs-6">
@L("TaskList") - <spandata-bind="text: abp.utils.formatString(localize('Xtasks'), tasks().length)"></span>
</h3>
<divclass="col-xs-6 text-right">
<selectdata-bind="value: selectedTaskState, event: { change: refreshTasks }">
<optionvalue="0">@L("AllTasks")</option>
<optionvalue="1">@L("ActiveTasks")</option>
<optionvalue="2">@L("CompletedTasks")</option>
</select>
</div>
</div>
</div>
<ulclass="list-group"data-bind="foreach: tasks">
<divclass="list-group-item">
<spanclass="task-state-icon glyphicon"data-bind="click: $parent.changeTaskState, css: { 'glyphicon-minus': state() == 1, 'glyphicon-ok': state() == 2 }"></span>
<spandata-bind="html: description(), css: { 'task-description-active': state() == 1, 'task-description-completed': state() == 2 }"></span>
<br/>
<spandata-bind="visible: assignedPersonId()">
<spanclass="task-assignedto"data-bind="text: assignedPersonName"></span>
</span>
<spanclass="task-creationtime"data-bind="text: moment(creationTime()).fromNow()"></span>
</div>
</ul>
</div>
此视图仅设计为与Twitter引导一起工作。CSS类是bootstrap的类。但这并不重要。这里有两点很重要: 首先:我们可以使用@L(“TaskList”)来获得一个本地化的字符串。方法是在AbpWebViewPage类中定义的(参见从AbpWebViewPage派生的SimpleTaskSystemWebViewPageBase类)。你可以在视图中使用LocalizationHelper.GetString(…)的快捷方式(详细信息请参阅本地化部分)。因为这是一个Razor视图,我们可以在视图中直接使用c#代码。因此,我们可以在服务器端创建动态HTML。记住,这个视图将只加载一次,因为这是一个SPA! 第二:我们可以使用敲除数据绑定atribute(如data-bind="foreach: tasks")和其他javascript方法(如abp.util . formatstring)来使用javascript视图模型的字段。因此,我们可以在客户端创建动态HTML。看到“点击:$的父母。这是用来绑定图标的点击事件到javascript视图模型代码中的changeTaskState函数。类似地,我们绑定了combobox的更改事件以刷新任务函数。更多信息,请参见knockout.js网站。 新任务 “新任务”视图相对简单。有一个任务描述和一个可选的人员选择: 该页面的视图模型如下: 隐藏,收缩,复制Code
define(['service!tasksystem/person', 'service!tasksystem/task', 'plugins/history'],
function (personService, taskService, history) { var localize = abp.localization.getSource('SimpleTaskSystem'); return function () {
var that = this; var _$view = null;
var _$form = null; that.people = ko.mapping.fromJS([]); that.task = {
description: ko.observable(''),
assignedPersonId: ko.observable(0)
}; that.canActivate = function () {
return personService.getAllPeople().done(function (data) {
ko.mapping.fromJS(data.people, that.people);
});
}; that.attached = function (view, parent) {
_$view = $(view);
_$form = _$view.find('form');
_$form.validate();
}; that.saveTask = function () {
if (!_$form.valid()) {
return;
} abp.ui.setBusy(_$view,
taskService.createTask(ko.mapping.toJS(that.task))
.done(function() {
abp.notify.info(abp.utils.formatString(localize("TaskCreatedMessage"), that.task.description()));
history.navigate('');
})
);
};
};
});
canActivate是Durandal的一种特殊功能。在这个函数中,你可以返回true/false来允许/阻止进入页面。杜兰达尔也接受了一个承诺。在本例中,它等待promise的结果来决定是否激活视图。你的诺言应该是真/假的。ABP覆盖此行为,以接受承诺的除真/假以外的任何其他数据类型。因此,我们可以直接返回从ABP的动态javascript代理返回的承诺(如上面canActivate方法所示)。 attach也是Durandal的另一个特殊函数,当视图附加到DOM(文档对象模型)并可以安全地操作它时,将调用该函数。 saveTask用于将给定任务保存到服务器。它首先验证表单,然后调用taskService。createTask函数(记住这个函数是由ABP自动动态创建的,并且它返回一个承诺)。在这里,您可以看到两个API调用,用于在将任务保存到服务器后显示通知。abp.ui。setBusy用于将DOM元素设置为busy(显示加载指示器并阻塞UI元素,在本例中为所有视图)。它接受了一个承诺se和取消繁忙时,承诺返回(失败或成功)。ABP为此使用了几个jQuery插件。 本地化 ABP提供了一个强大和灵活的定位系统。您可以将本地化文本存储在资源文件、XML文件甚至自定义源文件中。在本节中,我将展示如何使用XML文件。简单的任务系统项目包括XML文件在本地化文件夹: 这里是SimpleTaskSystem.xml的内容: 隐藏,复制Code
<?xmlversion="1.0"encoding="utf-8"?>
<localizationDictionaryculture="en">
<texts>
<textname="TaskSystem"value="Task System"/>
<textname="TaskList"value="Task List"/>
<textname="NewTask"value="New Task"/>
<textname="Xtasks"value="{0} tasks"/>
<textname="AllTasks"value="All tasks"/>
<textname="ActiveTasks"value="Active tasks"/>
<textname="CompletedTasks"value="Completed tasks"/>
<textname="TaskDescription"value="Task description"/>
<textname="EnterDescriptionHere"value="Task description"/>
<textname="AssignTo"value="Assign to"/>
<textname="SelectPerson"value="Select person"/>
<textname="CreateTheTask"value="Create the task"/>
<textname="TaskUpdatedMessage"value="Task has been successfully updated."/>
<textname="TaskCreatedMessage"value="Task {0} has been created successfully."/>
</texts>
</localizationDictionary>
它是一个简单的XML文件,包含所有可本地化文本的名称-值对。 属性定义文件的区域性。还有一个XML文件 土耳其(tr)本地化解决方案。本地化文件应该是 注册到ABP,以便在c#和javascript中使用: 隐藏,复制Code
Configuration.Localization.Sources.Add(
new XmlLocalizationSource(
"SimpleTaskSystem",
HttpContext.Current.Server.MapPath("~/Localization/SimpleTaskSystem")
)
);
本地化源必须是唯一的名称(SimpleTaskSystem) 这里)。因此,可以在应用程序中使用不同的源(以不同的格式和数据源存储)。XmlLocalizationSource还需要一个文件夹(/Localization/SimpleTaskSystem) (这里)读取本地化文件。 然后我们可以在需要时获得本地化文本。在c#中,我们有两个选项来获得本地化文本: 隐藏,复制Code
//Use directly
var s1 = LocalizationHelper.GetString("SimpleTaskSystem", "NewTask"); //Use after get source
var source = LocalizationHelper.GetSource("SimpleTaskSystem");
var s2 = source.GetString("NewTask");
它返回当前语言的本地化文本(通过使用当前线程的当前文化)。还有一些重写用于获取特定文化中的文本。在javascript中有一个类似的API来获取本地化文本: 隐藏,复制Code
//Use directly
var s1 = abp.localization.localize('NewTask', 'SimpleTaskSystem'); //Use after get source
var source = abp.localization.getSource('SimpleTaskSystem');
var s2 = source('NewTask');
这些方法还可以获取当前语言中的本地化文本。 Javascript API 在客户端,javascript中有一些每个应用程序都需要的通用功能。例如:显示成功通知、阻止ui元素、显示消息框等等。有许多库(jQuery插件)可以用于此目的。但它们都有不同的api。ASP。NET Boilerplate为这些任务定义了一些常见的api。因此,如果您想稍后更改通知插件,您只需要实现一个简单的API。此外,jQuery插件可以直接实现ABP api。您可以调用ABP的通知API,而不是直接调用插件的通知API。在这里,我将解释一些api。 日志API 当你想在客户端写一些简单的日志时,你可以使用console.log('…')API。但不是所有的浏览器都支持它,你的脚本可能会坏掉。所以,你应该先检查一下。另外,您可能想在其他地方写日志。ABP定义安全日志功能: 隐藏,复制Code
abp.log.debug('...');
abp.log.info('...');
abp.log.warn('...');
abp.log.error('...');
abp.log.fatal('...');
此外,您可以通过设置abb .log来更改日志级别。将abp.log水平到1。水平(例:abp.log.levels。INFO以避免编写调试日志)。默认情况下,这些函数将日志写入控制台。但是您可以很容易地重写这个行为。 通知API 我们喜欢在有事情发生时显示一些新奇的自动消失通知,比如当一个项目被保存或出现问题时。ABP为此定义了api: 隐藏,复制Code
abp.notify.success('a message text', 'optional title');
abp.notify.info('a message text', 'optional title');
abp.notify.warn('a message text', 'optional title');
abp.notify.error('a message text', 'optional title');
通知API在默认情况下由toastr库实现。您可以在您喜爱的通知库中实现它。 对话框的API Message API用于向用户显示消息。用户单击OK并关闭消息窗口/对话框。例子: 隐藏,复制Code
abp.message.info('some info message', 'some optional title');
abp.message.warn('some warning message', 'some optional title');
abp.message.error('some error message', 'some optional title');
目前还没有实施。您可以实现它来显示对话框或消息框。 UI块API 这个API用于阻塞整个页面或页面上的一个元素。因此,用户不能点击它。ABP API的有: 隐藏,复制Code
abp.ui.block(); //Block all page
abp.ui.block($('#MyDivElement')); //You can use any jQuery selection..
abp.ui.block('#MyDivElement'); //..or directly selector
abp.ui.unblock(); //Unblock all page
abp.ui.unblock('#MyDivElement'); //Unblock specific element
UI API忙 有时候你可能想让一些页面/元素忙起来。例如,您可能想要阻止一个表单,并在向服务器提交表单时显示busy指示器。ABP为此提供api: 隐藏,复制Code
abp.ui.setBusy('#MyRegisterForm');
abp.ui.clearBusy('#MyRegisterForm');
setBusy可以把一个承诺当作第二 参数以在承诺完成时自动调用clearBusy。请参阅示例项目(和本文)中的newtask ViewModel以了解使用情况。 其他人 其他有用的api将在将来被添加到ABP以标准化常见的任务。还有一些常用的实用函数(如ab .utils)。formatString的工作方式与string类似。格式在c#中)。 注意:如果你想实现这些api,在独立的文件中实现它们,并将这个独立的js文件包含到你的页面后的ab .js。 更多的 模块系统 ASP。NET样板被设计成模块化。它提供了创建可在不同应用程序中使用的公共模块的基础设施。一个模块可以依赖于其他模块。应用程序是由模块组成的。模块是包含从AbpModule派生的模块类的程序集。在本文解释的示例应用程序中,所有层都定义为独立的模块。例如,应用层定义如下模块: 隐藏,复制Code
/// <summary>
/// 'Application layer module' for this project.
/// </summary>
[DependsOn(typeof(SimpleTaskSystemCoreModule))]
public class SimpleTaskSystemApplicationModule : AbpModule
{
public override void Initialize()
{
//This code is used to register classes to dependency injection system for this assembly using conventions.
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); //We must declare mappings to be able to use AutoMapper
DtoMappings.Map();
}
}
ASP。NET Boilerplate在应用程序启动时分别调用模块的预初始化、初始化和后初始化方法。如果模块A依赖于模块B,则模块B在模块A之前初始化。所有方法的确切顺序:PreInitialize-B, PreI初始化- a,初始化- b,初始化- a,后初始化- b和后初始化- a。这对所有依赖图都成立。 Initialize是应该放置依赖注入配置的方法。在这里,您可以看到这个模块将其程序集中的所有类注册为常规的(参见下一节)。然后它使用AutoMapper库(特定于此应用程序)映射类。这个模块还定义了依赖关系(应用层只依赖于应用程序的域(核心)层)。 依赖注入和约定 ASP。当您通过遵循最佳实践和一些约定来编写应用程序时,NET Boilerplate几乎使依赖注入系统不可见。它自动注册所有存储库、域服务、应用程序服务、MVC控制器和Web API控制器。例如,你可能有一个IPersonAppService接口和实现它的PersonAppService类: 隐藏,复制Code
public interface IPersonAppService : IApplicationService
{
//...
} public class PersonAppService : IPersonAppService
{
//...
}
ASP。NET Boilerplate自动注册它,因为它实现了IApplicationService接口(它只是一个空接口)。它被注册为瞬态(每次使用创建实例)。当您将IPersonAppService接口注入(使用构造函数注入)到类时,将创建PersonAppService对象并自动传递到构造函数中。请参阅有关依赖注入及其在ASP中的实现的详细文档。净样板。 关于Angularjs,EntityFramework样本 我写了一篇文章“使用AngularJs, ASP。NET MVC, Web API和EntityFramework来构建单层网页的Web应用程序。EntityFramework。 总结 在本文中,我介绍了一个新的应用程序框架:ASP。净样板。它是使用最佳实践和最流行的工具开发现代web应用程序的起点。由于它是相当新的,可能会有一些缺失的概念。但它不会限制你。你可以开始点和buıild您的应用程序。它是新的,正在开发和扩展。我正积极地在公司的生产中使用它。 您可以向github页面(https://github.com/aspnetboilerplate/aspnetboilerplate/issues)编写bug报告、问题和特性请求。由于它是一个开源项目,所以您可以派生它并发送pull请求来为ABP源代码做出贡献。我想让它成为所有。net开发人员的一个起点,所以,一起开发它将是件好事。 文章历史 2016-07-19:更新了ABP的文章和源代码 v0.10。2014-11-02:更新了ABP的文章和源代码 v0.4.1。2014-09-06:文章完整修改和更新,使用sample application for ABP v0.3.2。2014-07-26:更新了ABP v0.3.0.1的文章和源代码。2014-07-01:添加AngularJs &解释的其他文章链接EntityFramework集成。2014-06-16:添加Angularjs&EntityFramework sample。更新到ABP v0.2.3。2014-05-22:更新样例项目到最新的ABP版本。2014-05-20:在文章中添加章节。2014-05-13:增加了文章的细节和章节。2014-05-08:增加了文章的细节和章节。2014-05-05:文章首次发表。 参考文献 [1] ASP。NET Boilerplate官方网站:http://www.aspnetboilerplate.com [2]书:“领域驱动设计:解决软件核心的复杂性”,作者:Eric Evans。 马丁福勒的网站:http://martinfowler.com [4] Fleunt Migrator: https://github.com/schambers/fluentmigrator [5] FluentNHibernate: http://www.fluentnhibernate.org [6] AutoMapper: http://automapper.org/r.org/ [7] Durandaljs: http://durandaljs.com/ [8]淘汰赛。js: http://knockoutjs.com/ 本文转载于:http://www.diyabc.com/frontweb/news19549.html