mirror of https://github.com/abpframework/abp.git
committed by
GitHub
10 changed files with 407 additions and 8 deletions
@ -1 +1,198 @@ |
|||||
TODO .... |
## ASP.NET Core {{UI_Value}} 教程 - 第三章 |
||||
|
````json |
||||
|
//[doc-params] |
||||
|
{ |
||||
|
"UI": ["MVC","NG"] |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
{{ |
||||
|
if UI == "MVC" |
||||
|
DB="ef" |
||||
|
DB_Text="Entity Framework Core" |
||||
|
UI_Text="mvc" |
||||
|
else if UI == "NG" |
||||
|
DB="mongodb" |
||||
|
DB_Text="MongoDB" |
||||
|
UI_Text="angular" |
||||
|
else |
||||
|
DB ="?" |
||||
|
UI_Text="?" |
||||
|
end |
||||
|
}} |
||||
|
|
||||
|
### 关于本教程 |
||||
|
|
||||
|
这是ASP.NET Core{{UI_Value}}系列教程的第二章. 共有三章: |
||||
|
|
||||
|
- [Part-1: 创建项目和书籍列表页面](Part-1.md) |
||||
|
- [Part 2: 创建,编辑,删除书籍](Part-2.md) |
||||
|
- **Part-3: 集成测试(本章)** |
||||
|
|
||||
|
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). |
||||
|
|
||||
|
### 解决方案中的测试项目 |
||||
|
|
||||
|
解决方案中有多个测试项目: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: |
||||
|
|
||||
|
* [xunit](https://xunit.github.io/) 作为主测试框架. |
||||
|
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. |
||||
|
* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. |
||||
|
|
||||
|
### 添加测试数据 |
||||
|
|
||||
|
启动模板包含`Acme.BookStore.TestBase`项目中的`BookStoreTestDataSeedContributor`类,它创建一些数据来运行测试. |
||||
|
更改`BookStoreTestDataSeedContributor`类如下所示: |
||||
|
|
||||
|
````csharp |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
using Volo.Abp.Guids; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookStoreTestDataSeedContributor |
||||
|
: IDataSeedContributor, ITransientDependency |
||||
|
{ |
||||
|
private readonly IRepository<Book, Guid> _bookRepository; |
||||
|
private readonly IGuidGenerator _guidGenerator; |
||||
|
|
||||
|
public BookStoreTestDataSeedContributor( |
||||
|
IRepository<Book, Guid> bookRepository, |
||||
|
IGuidGenerator guidGenerator) |
||||
|
{ |
||||
|
_bookRepository = bookRepository; |
||||
|
_guidGenerator = guidGenerator; |
||||
|
} |
||||
|
|
||||
|
public async Task SeedAsync(DataSeedContext context) |
||||
|
{ |
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book(id: _guidGenerator.Create(), |
||||
|
name: "Test book 1", |
||||
|
type: BookType.Fantastic, |
||||
|
publishDate: new DateTime(2015, 05, 24), |
||||
|
price: 21 |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book(id: _guidGenerator.Create(), |
||||
|
name: "Test book 2", |
||||
|
type: BookType.Science, |
||||
|
publishDate: new DateTime(2014, 02, 11), |
||||
|
price: 15 |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 注入`IRepository<Book,Guid>`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. |
||||
|
* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../Guid-Generation.md)了解更多信息). |
||||
|
|
||||
|
### 测试 BookAppService |
||||
|
|
||||
|
在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: |
||||
|
|
||||
|
````csharp |
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Xunit; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.Validation; |
||||
|
using Microsoft.EntityFrameworkCore.Internal; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookAppService_Tests : BookStoreApplicationTestBase |
||||
|
{ |
||||
|
private readonly IBookAppService _bookAppService; |
||||
|
|
||||
|
public BookAppService_Tests() |
||||
|
{ |
||||
|
_bookAppService = GetRequiredService<IBookAppService>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Get_List_Of_Books() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.GetListAsync( |
||||
|
new PagedAndSortedResultRequestDto() |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.TotalCount.ShouldBeGreaterThan(0); |
||||
|
result.Items.ShouldContain(b => b.Name == "Test book 1"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. |
||||
|
|
||||
|
新增测试方法,用以测试创建一个合法book实体的场景: |
||||
|
|
||||
|
````C# |
||||
|
[Fact] |
||||
|
public async Task Should_Create_A_Valid_Book() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "New test book 42", |
||||
|
Price = 10, |
||||
|
PublishDate = DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.Id.ShouldNotBe(Guid.Empty); |
||||
|
result.Name.ShouldBe("New test book 42"); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
新增测试方法,用以测试创建一个非法book实体失败的场景: |
||||
|
|
||||
|
````csharp |
||||
|
[Fact] |
||||
|
public async Task Should_Not_Create_A_Book_Without_Name() |
||||
|
{ |
||||
|
var exception = await Assert.ThrowsAsync<Volo.Abp.Validation.AbpValidationException>(async () => |
||||
|
{ |
||||
|
await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "", |
||||
|
Price = 10, |
||||
|
PublishDate = DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
exception.ValidationErrors |
||||
|
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. |
||||
|
|
||||
|
打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
恭喜, 绿色图标表示测试已成功通过! |
||||
|
|||||
@ -0,0 +1,197 @@ |
|||||
|
# 修改菜单 |
||||
|
|
||||
|
|
||||
|
菜单在 @abp/ng.theme.basic包 `ApplicationLayoutComponent` 内部. 有几种修改菜单的方法,本文档介绍了这些方法. 如果你想完全替换菜单,请参考[组件替换文档]了解如何替换布局. |
||||
|
|
||||
|
<!-- TODO: Replace layout replacement document with component replacement. Layout replacement document will be created.--> |
||||
|
|
||||
|
## 如何添加Logo |
||||
|
|
||||
|
环境变量中的 `logoUrl` 是logo的url. |
||||
|
|
||||
|
你可以在 `src/assets` 文件夹下添加logo并设置 `logoUrl`: |
||||
|
|
||||
|
```js |
||||
|
export const environment = { |
||||
|
// other configurations |
||||
|
application: { |
||||
|
name: 'MyProjectName', |
||||
|
logoUrl: 'assets/logo.png', |
||||
|
}, |
||||
|
// other configurations |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
## 如何添加导航元素 |
||||
|
|
||||
|
### 通过 AppRoutingModule 中的 `routes` 属性 |
||||
|
|
||||
|
你可以通过在 `app-routing.module` 中将路由作为子属性添加到路由配置的 `data` 属性来定义路由. `@abp/ng.core` 包组织路由并将其存储在 `ConfigState` 中.`ApplicationLayoutComponent` 从存储中获取路由显示在菜单上. |
||||
|
|
||||
|
你可以像以下一样添加 `routes` 属性: |
||||
|
|
||||
|
```js |
||||
|
{ |
||||
|
path: 'your-path', |
||||
|
data: { |
||||
|
routes: { |
||||
|
name: 'Your navigation', |
||||
|
order: 3, |
||||
|
iconClass: 'fas fa-question-circle', |
||||
|
requiredPolicy: 'permission key here', |
||||
|
children: [ |
||||
|
{ |
||||
|
path: 'child', |
||||
|
name: 'Your child navigation', |
||||
|
order: 1, |
||||
|
requiredPolicy: 'permission key here', |
||||
|
}, |
||||
|
], |
||||
|
} as ABP.Route, // can be imported from @abp/ng.core |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- `name` 是导航元素的标签,可以传递本地化密钥或本地化对象. |
||||
|
- `order` 排序导航元素. |
||||
|
- `iconClass` 是 `i` 标签的类,在导航标签的左侧. |
||||
|
- `requiredPolicy` 是访问页面所需的权限key. 参阅 [权限管理文档](./Permission-Management.md) |
||||
|
- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property. |
||||
|
- `children` 是一个数组,用于声明子菜单,它基于给定的 `path` 属性,路径是在`/your-path/child`. |
||||
|
|
||||
|
添加了上面描述的route属性后,导航菜单如下图所示: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 通过 ConfigState |
||||
|
|
||||
|
`ConfigStateService` 的 `dispatchAddRoute` 方法可以向菜单添加一个新的导航元素. |
||||
|
|
||||
|
```js |
||||
|
// this.config is instance of ConfigStateService |
||||
|
|
||||
|
const newRoute: ABP.Route = { |
||||
|
name: 'My New Page', |
||||
|
iconClass: 'fa fa-dashboard', |
||||
|
path: 'page', |
||||
|
invisible: false, |
||||
|
order: 2, |
||||
|
requiredPolicy: 'MyProjectName.MyNewPage', |
||||
|
} as Omit<ABP.Route, 'children'>; |
||||
|
|
||||
|
this.config.dispatchAddRoute(newRoute); |
||||
|
// returns a state stream which emits after dispatch action is complete |
||||
|
``` |
||||
|
|
||||
|
`newRoute` 放在根级别,没有任何父路由,url将为`/path`. |
||||
|
|
||||
|
如果你想 **添加子路由, 你可以这样做:** |
||||
|
|
||||
|
```js |
||||
|
// this.config is instance of ConfigStateService |
||||
|
// eIdentityRouteNames enum can be imported from @abp/ng.identity |
||||
|
|
||||
|
const newRoute: ABP.Route = { |
||||
|
parentName: eIdentityRouteNames.IdentityManagement, |
||||
|
name: 'My New Page', |
||||
|
iconClass: 'fa fa-dashboard', |
||||
|
path: 'page', |
||||
|
invisible: false, |
||||
|
order: 3, |
||||
|
requiredPolicy: 'MyProjectName.MyNewPage' |
||||
|
} as Omit<ABP.Route, 'children'>; |
||||
|
|
||||
|
this.config.dispatchAddRoute(newRoute); |
||||
|
// returns a state stream which emits after dispatch action is complete |
||||
|
``` |
||||
|
|
||||
|
`newRoute` 做为 `eIdentityRouteNames.IdentityManagement` 的子路由添加, url 设置为 `'/identity/page'`. |
||||
|
|
||||
|
新路由看起来像这样: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 如何修改一个导航元素 |
||||
|
|
||||
|
`DispatchPatchRouteByName` 方法通过名称查找路由,并使用二个参数传递的新配置替换存储中的配置. |
||||
|
|
||||
|
```js |
||||
|
// this.config is instance of ConfigStateService |
||||
|
// eIdentityRouteNames enum can be imported from @abp/ng.identity |
||||
|
|
||||
|
const newRouteConfig: Partial<ABP.Route> = { |
||||
|
iconClass: 'fas fa-home', |
||||
|
parentName: eIdentityRouteNames.Administration, |
||||
|
order: 0, |
||||
|
children: [ |
||||
|
{ |
||||
|
name: 'Dashboard', |
||||
|
path: 'dashboard', |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig); |
||||
|
// returns a state stream which emits after dispatch action is complete |
||||
|
``` |
||||
|
|
||||
|
* 根据给定的 `parentName` 将 _Home_ 导航移动到 _Administration_ 下拉框下. |
||||
|
* 添加了 icon. |
||||
|
* 指定了顺序. |
||||
|
* 添加了名为 _Dashboard_ 的子路由. |
||||
|
|
||||
|
修改后,导航元素看起来像这样: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 如何在菜单的右侧添加元素 |
||||
|
|
||||
|
右侧的元素存储在 @abp/ng.theme.basic 包的 `LayoutState` 中. |
||||
|
|
||||
|
`LayoutStateService` 的 `dispatchAddNavigationElement` 方法添加元素到右侧的菜单. |
||||
|
|
||||
|
你可以通过将模板添加到 `app.component` 调用 `dispatchAddNavigationElement` 方法来插入元素: |
||||
|
|
||||
|
```js |
||||
|
import { Layout, LayoutStateService } from '@abp/ng.theme.basic'; // added this line |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-root', |
||||
|
template: ` |
||||
|
|
||||
|
<!-- Added below content --> |
||||
|
<ng-template #search |
||||
|
><input type="search" placeholder="Search" class="bg-transparent border-0" |
||||
|
/></ng-template> |
||||
|
`, |
||||
|
}) |
||||
|
export class AppComponent { |
||||
|
// Added ViewChild |
||||
|
@ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef<any>; |
||||
|
|
||||
|
constructor(private layout: LayoutStateService) {} // injected LayoutStateService |
||||
|
|
||||
|
// Added ngAfterViewInit |
||||
|
ngAfterViewInit() { |
||||
|
const newElement = { |
||||
|
name: 'Search', |
||||
|
element: this.searchElementRef, |
||||
|
order: 1, |
||||
|
} as Layout.NavigationElement; |
||||
|
|
||||
|
this.layout.dispatchAddNavigationElement(newElement); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
上面我们在菜单添加了一个搜索输入,最终UI如下:s |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 如何删除右侧菜单元素 |
||||
|
|
||||
|
TODO |
||||
|
|
||||
|
## 下一步是什么? |
||||
|
|
||||
|
* [组件替换](./Component-Replacement.md) |
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 44 KiB |
Loading…
Reference in new issue