@ -1,4 +1,4 @@ |
|||
name: "Main" |
|||
name: "build and test" |
|||
on: |
|||
pull_request: |
|||
paths: |
|||
@ -0,0 +1,237 @@ |
|||
# SignalR Integration |
|||
|
|||
> It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides a SignalR integration packages those simplify the integration and usage. |
|||
|
|||
## Installation |
|||
|
|||
### Server Side |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. |
|||
|
|||
#### Using the ABP CLI |
|||
|
|||
Open a command line window in the folder of your project (.csproj file) and type the following command: |
|||
|
|||
```bash |
|||
abp add-package Volo.Abp.AspNetCore.SignalR |
|||
``` |
|||
|
|||
> You typically want to add this package to the web or API layer of your application, depending on your architecture. |
|||
|
|||
#### Manual Installation |
|||
|
|||
If you want to manually install; |
|||
|
|||
1. Add the [Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR) NuGet package to your project: |
|||
|
|||
``` |
|||
Install-Package Volo.Abp.AspNetCore.SignalR |
|||
``` |
|||
|
|||
Or use the Visual Studio NuGet package management UI to install it. |
|||
|
|||
2. Add the `AbpAspNetCoreSignalRModule` to the dependency list of your module: |
|||
|
|||
```csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
``` |
|||
|
|||
> You don't need to use the `services.AddSignalR()` and the `app.UseEndpoints(...)`, it's done by the `AbpAspNetCoreSignalRModule`. |
|||
|
|||
### Client Side |
|||
|
|||
Client side installation depends on your UI framework / client type. |
|||
|
|||
#### ASP.NET Core MVC / Razor Pages UI |
|||
|
|||
Run the following command in the root folder of your web project: |
|||
|
|||
````bash |
|||
yarn add @abp/signalr |
|||
```` |
|||
|
|||
> This requires to [install yarn](https://yarnpkg.com/) if you haven't install before. |
|||
|
|||
This will add the `@abp/signalr` to the dependencies in the `package.json` of your project: |
|||
|
|||
````json |
|||
{ |
|||
... |
|||
"dependencies": { |
|||
... |
|||
"@abp/signalr": "~2.7.0" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Run the `gulp` in the root folder of your web project: |
|||
|
|||
````bash |
|||
gulp |
|||
```` |
|||
|
|||
This will copy the SignalR JavaScript files into your project: |
|||
|
|||
 |
|||
|
|||
Finally, add the following code to your page/view to include the `signalr.js` file |
|||
|
|||
````xml |
|||
@section scripts { |
|||
<abp-script type="typeof(SignalRBrowserScriptContributor)" /> |
|||
} |
|||
```` |
|||
|
|||
It requires to add `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` to your page/view. |
|||
|
|||
> You could add the `signalr.js` file in a standard way. But using the `SignalRBrowserScriptContributor` has additional benefits. See the [Client Side Package Management](UI/AspNetCore/Client-Side-Package-Management.md) and [Bundling & Minification](UI/AspNetCore/Bundling-Minification.md) documents for details. |
|||
|
|||
That's all. you can use the [SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client) in your page. |
|||
|
|||
#### Other UI Frameworks / Clients |
|||
|
|||
Please refer to [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) for other type of clients. |
|||
|
|||
## The ABP Framework Integration |
|||
|
|||
This section covers the additional benefits when you use the ABP Framework integration packages. |
|||
|
|||
### Hub Route & Mapping |
|||
|
|||
ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) (as transient) and maps the hub endpoint. So, you don't have to use the ` app.UseEndpoints(...)` to map your hubs. Hub route (URL) is determined conventionally based on your hub name. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class MessagingHub : Hub |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
The hub route will be `/signalr-hubs/messasing` for the `MessasingHub`: |
|||
|
|||
* Adding a standard `/signalr-hubs/` prefix |
|||
* Continue with the **camel case** hub name, without the `Hub` suffix. |
|||
|
|||
If you want to specify the route, you can use the `HubRoute` attribute: |
|||
|
|||
````csharp |
|||
[HubRoute("/my-messasing-hub")] |
|||
public class MessagingHub : Hub |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
### AbpHub Base Classes |
|||
|
|||
Instead of the standard `Hub` and `Hub<T>` classes, you can inherit from the `AbpHub` or `AbpHub<T>` which hve useful base properties like `CurrentUser`. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class MessagingHub : AbpHub |
|||
{ |
|||
public async Task SendMessage(string targetUserName, string message) |
|||
{ |
|||
var currentUserName = CurrentUser.UserName; //Access to the current user info |
|||
var txt = L["MyText"]; //Localization |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> While you could inject the same properties into your hub constructor, this way simplifies your hub class. |
|||
|
|||
### Manual Registration / Mapping |
|||
|
|||
ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) as a **transient service**. If you want to **disable auto dependency injection** registration for your hub class, just add a `DisableConventionalRegistration` attribute. You can still register your hub class to dependency injection in the `ConfigureServices` method of your module if you like: |
|||
|
|||
````csharp |
|||
context.Services.AddTransient<MessagingHub>(); |
|||
```` |
|||
|
|||
When **you or ABP** register the class to the dependency injection, it is automatically mapped to the endpoint route configuration just as described in the previous sections. You can use `DisableAutoHubMap` attribute if you want to manually map your hub class. |
|||
|
|||
For manual mapping, you have two options: |
|||
|
|||
1. Use the `AbpSignalROptions` to add your map configuration (in the `ConfigureServices` method of your [module](Module-Development-Basics.md)), so ABP still performs the endpoint mapping for your hub: |
|||
|
|||
````csharp |
|||
Configure<AbpSignalROptions>(options => |
|||
{ |
|||
options.Hubs.Add( |
|||
new HubConfig( |
|||
typeof(MessagingHub), //Hub type |
|||
"/my-messaging/route", //Hub route (URL) |
|||
hubOptions => |
|||
{ |
|||
//Additional options |
|||
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
} |
|||
) |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
This is a good way to provide additional SignalR options. |
|||
|
|||
If you don't want to disable auto hub map, but still want to perform additional SignalR configuration, use the `options.Hubs.AddOrUpdate(...)` method: |
|||
|
|||
````csharp |
|||
Configure<AbpSignalROptions>(options => |
|||
{ |
|||
options.Hubs.AddOrUpdate( |
|||
typeof(MessagingHub), //Hub type |
|||
config => //Additional configuration |
|||
{ |
|||
config.RoutePattern = "/my-messaging-hub"; //override the default route |
|||
config.ConfigureActions.Add(hubOptions => |
|||
{ |
|||
//Additional options |
|||
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
}); |
|||
} |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
This is the way you can modify the options of a hub class defined in a depended module (where you don't have the source code access). |
|||
|
|||
2. Change `app.UseConfiguredEndpoints` in the `OnApplicationInitialization` method of your [module](Module-Development-Basics.md) as shown below (added a lambda method as the parameter). |
|||
|
|||
````csharp |
|||
app.UseConfiguredEndpoints(endpoints => |
|||
{ |
|||
endpoints.MapHub<MessagingHub>("/my-messaging-hub", options => |
|||
{ |
|||
options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
### UserIdProvider |
|||
|
|||
ABP implements SignalR's `IUserIdProvider` interface to provide the current user id from the `ICurrentUser` service of the ABP framework (see [the current user service](CurrentUser.md)), so it will be integrated to the authentication system of your application. The implementing class is the `AbpSignalRUserIdProvider`, if you want to change/override it. |
|||
|
|||
## Example Application |
|||
|
|||
See the [SignalR Integration Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) as a sample application. It has a simple Chat page to send messages between (authenticated) users. |
|||
|
|||
 |
|||
|
|||
## Remarks |
|||
|
|||
ABP Framework doesn't change the SignalR. It works in your ABP Framework based application just like any other ASP.NET Core application. |
|||
|
|||
Refer to the Microsoft's documentation to [host and scale](https://docs.microsoft.com/en-us/aspnet/core/signalr/scale) your application, integrate to [Azure](https://docs.microsoft.com/en-us/aspnet/core/signalr/publish-to-azure-web-app) or [Redis backplane](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)... etc. |
|||
|
|||
## See Also |
|||
|
|||
* [Microsoft SignalR documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) |
|||
@ -0,0 +1,165 @@ |
|||
# Querying Lists Easily with ListService |
|||
|
|||
`ListService` is a utility service to provide an easy pagination, sorting, and search implementation. |
|||
|
|||
|
|||
|
|||
## Getting Started |
|||
|
|||
`ListService` is **not provided in root**. The reason is, this way, it will clear any subscriptions on component destroy. You may use the optional `LIST_QUERY_DEBOUNCE_TIME` token to adjust the debounce behavior. |
|||
|
|||
```js |
|||
import { ListService } from '@abp/ng.core'; |
|||
import { BookDto } from '../models'; |
|||
import { BookService } from '../services'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
providers: [ |
|||
// [Required] |
|||
ListService, |
|||
|
|||
// [Optional] |
|||
// Provide this token if you want a different debounce time. |
|||
// Default is 300. Cannot be 0. Any value below 100 is not recommended. |
|||
{ provide: LIST_QUERY_DEBOUNCE_TIME, useValue: 500 }, |
|||
], |
|||
template: ` |
|||
|
|||
`, |
|||
}) |
|||
class BookComponent { |
|||
items: BookDto[] = []; |
|||
count = 0; |
|||
|
|||
constructor( |
|||
public readonly list: ListService, |
|||
private bookService: BookService, |
|||
) {} |
|||
|
|||
ngOnInit() { |
|||
// A function that gets query and returns an observable |
|||
const bookStreamCreator = query => this.bookService.getList(query); |
|||
|
|||
this.list.hookToQuery(bookStreamCreator).subscribe( |
|||
response => { |
|||
this.items = response.items; |
|||
this.count = response.count; |
|||
// If you use OnPush change detection strategy, |
|||
// call detectChanges method of ChangeDetectorRef here. |
|||
} |
|||
); // Subscription is auto-cleared on destroy. |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> Noticed `list` is `public` and `readonly`? That is because we will use `ListService` properties directly in the component's template. That may be considered as an anti-pattern, but it is much quicker to implement. You can always use public component properties instead. |
|||
|
|||
Place `ListService` properties into the template like this: |
|||
|
|||
```html |
|||
<abp-table |
|||
[value]="book.items" |
|||
[(page)]="list.page" |
|||
[rows]="list.maxResultCount" |
|||
[totalRecords]="book.totalCount" |
|||
[headerTemplate]="tableHeader" |
|||
[bodyTemplate]="tableBody" |
|||
[abpLoading]="list.isLoading$ | async" |
|||
> |
|||
</abp-table> |
|||
|
|||
<ng-template #tableHeader> |
|||
<tr> |
|||
<th (click)="nameSort.sort('name')"> |
|||
{%{{{ '::Name' | abpLocalization }}}%} |
|||
<abp-sort-order-icon |
|||
#nameSort |
|||
sortKey="name" |
|||
[(selectedSortKey)]="list.sortKey" |
|||
[(order)]="list.sortOrder" |
|||
></abp-sort-order-icon> |
|||
</th> |
|||
</tr> |
|||
</ng-template> |
|||
|
|||
<ng-template #tableBody let-data> |
|||
<tr> |
|||
<td>{%{{{ data.name }}}%}</td> |
|||
</tr> |
|||
</ng-template> |
|||
``` |
|||
|
|||
## Usage with Observables |
|||
|
|||
You may use observables in combination with [AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe) of Angular instead. Here are some possibilities: |
|||
|
|||
```ts |
|||
book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query)); |
|||
``` |
|||
|
|||
```html |
|||
<!-- simplified representation of the template --> |
|||
|
|||
<abp-table |
|||
[value]="(book$ | async)?.items || []" |
|||
[totalRecords]="(book$ | async)?.totalCount" |
|||
> |
|||
</abp-table> |
|||
|
|||
<!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE --> |
|||
``` |
|||
|
|||
...or... |
|||
|
|||
|
|||
```ts |
|||
@Select(BookState.getBooks) |
|||
books$: Observable<BookDto[]>; |
|||
|
|||
@Select(BookState.getBookCount) |
|||
bookCount$: Observable<number>; |
|||
|
|||
ngOnInit() { |
|||
this.list.hookToQuery((query) => this.store.dispatch(new GetBooks(query))).subscribe(); |
|||
} |
|||
``` |
|||
|
|||
```html |
|||
<!-- simplified representation of the template --> |
|||
|
|||
<abp-table |
|||
[value]="books$ | async" |
|||
[totalRecords]="bookCount$ | async" |
|||
> |
|||
</abp-table> |
|||
``` |
|||
|
|||
## How to Refresh Table on Create/Update/Delete |
|||
|
|||
`ListService` exposes a `get` method to trigger a request with the current query. So, basically, whenever a create, update, or delete action resolves, you can call `this.list.get();` and it will call hooked stream creator again. |
|||
|
|||
```ts |
|||
this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get); |
|||
``` |
|||
|
|||
...or... |
|||
|
|||
```ts |
|||
this.bookService.createByInput(form.value) |
|||
.subscribe(() => { |
|||
this.list.get(); |
|||
|
|||
// Other subscription logic here |
|||
}) |
|||
``` |
|||
|
|||
## How to Implement Server-Side Search in a Table |
|||
|
|||
`ListService` exposes a `filter` property that will trigger a request with the current query and the given search string. All you need to do is to bind it to an input element with two-way binding. |
|||
|
|||
```html |
|||
<!-- simplified representation --> |
|||
|
|||
<input type="text" name="search" [(ngModel)]="list.filter"> |
|||
``` |
|||
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 45 KiB |
@ -0,0 +1,62 @@ |
|||
# 示例应用 |
|||
|
|||
这些是ABP框架创建的官方示例. 这些示例大部分在[abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub 仓库. |
|||
|
|||
### 微服务示例 |
|||
|
|||
演示如何基于微服务体系结构构建系统的完整解决方案. |
|||
|
|||
* [示例的文档](Microservice-Demo.md) |
|||
* [源码](https://github.com/abpframework/abp/tree/dev/samples/MicroserviceDemo) |
|||
* [微服务架构文档](../Microservice-Architecture.md) |
|||
|
|||
### Book Store |
|||
|
|||
一个简单的CRUD应用程序,展示了使用ABP框架开发应用程序的基本原理. 使用不同的技术实现了相同的示例: |
|||
|
|||
* **Book Store: Razor Pages UI & Entity Framework Core** |
|||
|
|||
* [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC) |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore) |
|||
|
|||
* **Book Store: Angular UI & MongoDB** |
|||
|
|||
* [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
|||
|
|||
* **Book Store: Modular application (Razor Pages UI & EF Core)** |
|||
|
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular) |
|||
|
|||
如果没有Razor Pages & MongoDB 结合,但你可以检查两个文档来理解它,因为DB和UI不会互相影响. |
|||
|
|||
### 其他示例 |
|||
|
|||
* **Entity Framework 迁移**: 演示如何将应用程序拆分为多个数据库的解决方案. 每个数据库包含不同的模块. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) |
|||
* [EF Core数据库迁移文档](../Entity-Framework-Core-Migrations.md) |
|||
* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) |
|||
* [SignalR 集成文档](../SignalR-Integration.md) |
|||
* **Dashboard Demo**: 一个简单的应用程序,展示了如何在ASP.NET Core MVC UI中使用widget系统. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) |
|||
* [Widget 文档](../UI/AspNetCore/Widgets.md) |
|||
* **RabbitMQ 事件总线 Demo**: 由两个通过RabbitMQ集成的分布式事件相互通信的应用程序组成的解决方案. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus) |
|||
* [分布式事件总线文档](../Distributed-Event-Bus.md) |
|||
* [RabbitMQ 分布式事件总线集成文档](../Distributed-Event-Bus-RabbitMQ-Integration.md) |
|||
* **文本模板 Demo**: 文本模板系统的不同用例. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) |
|||
* [文本模板文档](../Text-Templating.md) |
|||
* **自定义认证**: 如何为ASP.NET Core MVC / Razor Pages应用程序自定义身份验证的解决方案. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization) |
|||
* 相关 "[How To](../How-To/Index.md)" 文档: |
|||
* [Azure Active Directory 认证](../How-To/Azure-Active-Directory-Authentication-MVC.md) |
|||
* [自定义登录页面](../How-To/Customize-Login-Page-MVC.md) |
|||
* [自定义 SignIn Manager](../How-To/Customize-SignIn-Manager.md) |
|||
* **空的ASP.NET Core应用程序**: 从基本的ASP.NET Core应用程序使用ABP框架. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) |
|||
* [文档](../Getting-Started-AspNetCore-Application.md) |
|||
* **空的控制台应用程序**: 从基本的控制台应用程序安装ABP框架. |
|||
* [源码](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication) |
|||
* [文档](../Getting-Started-Console-Application.md) |
|||
@ -0,0 +1,227 @@ |
|||
# SignalR 集成 |
|||
|
|||
> 你可以按照[标准的微软教程](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signal)添加[SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction)到你的应用程序,但ABP提供了简化集成的SignalR集成包. |
|||
|
|||
## 安装 |
|||
|
|||
### 服务器端 |
|||
|
|||
建议使用[ABP CLI](CLI.md)安装包. |
|||
|
|||
#### 使用 ABP CLI |
|||
|
|||
在项目的文件夹(.csproj文件)中打开命令行窗口,然后输入以下命令: |
|||
|
|||
```bash |
|||
abp add-package Volo.Abp.AspNetCore.SignalR |
|||
``` |
|||
|
|||
> 你通常需要将此软件包添加到应用程序的Web或API层,具体取决于你的架构. |
|||
|
|||
#### 手动安装 |
|||
|
|||
如果你想手动安装: |
|||
|
|||
1. 添加[Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR)NuGet包到你的项目: |
|||
|
|||
``` |
|||
Install-Package Volo.Abp.AspNetCore.SignalR |
|||
``` |
|||
|
|||
或者使用VisualStudio提供的UI安装 |
|||
|
|||
2. 添加 `AbpAspNetCoreSignalRModule` 到你的模块的依赖列表. |
|||
|
|||
```csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
``` |
|||
|
|||
> 你不需要 `services.AddSignalR()` 和 `app.UseEndpoints(...)`,它们在 `AbpAspNetCoreSignalRModule` 中已经添加了. |
|||
|
|||
### 客户端 |
|||
|
|||
客户端安装取决于你的UI框架/客户端类型. |
|||
|
|||
#### ASP.NET Core MVC / Razor Pages UI |
|||
|
|||
在你的Web项目的根文件夹中运行以下命令: |
|||
|
|||
````bash |
|||
yarn add @abp/signalr |
|||
```` |
|||
|
|||
> 需要 [yarn](https://yarnpkg.com/) 环境. |
|||
|
|||
它会添加 `@abp/signalr` 到你的项目中的 `package.json` 依赖项: |
|||
|
|||
````json |
|||
{ |
|||
... |
|||
"dependencies": { |
|||
... |
|||
"@abp/signalr": "~2.7.0" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
在你的Web项目的根文件夹中运行 `gulp`: |
|||
|
|||
````bash |
|||
gulp |
|||
```` |
|||
|
|||
它会将SignalR JavaScript文件拷贝到你的项目: |
|||
|
|||
 |
|||
|
|||
最后将以下代码添加到页面/视图中, 添加包含 `signalr.js` 文件: |
|||
|
|||
````xml |
|||
@section scripts { |
|||
<abp-script type="typeof(SignalRBrowserScriptContributor)" /> |
|||
} |
|||
```` |
|||
|
|||
它需要将 `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` 添加到你的页面/视图. |
|||
|
|||
> 你可以用标准方式添加 `signalr.js` 文件. 但是使用 `SignalRBrowserScriptContributor` 具有其他好处. 有关详细信息,请参见[客户端程序包管理](UI/AspNetCore/Client-Side-Package-Management.md)和[捆绑和压缩文档](UI/AspNetCore/Bundling-Minification.md). |
|||
|
|||
这就是全部了,你可以在你的页面使用[SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client). |
|||
|
|||
#### 其他的UI框架/客户端 |
|||
|
|||
其他类型的客户端请参考[微软文档](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction). |
|||
|
|||
## ABP框架集成 |
|||
|
|||
本节介绍了使用ABP框架集成包的其他好处. |
|||
|
|||
### Hub 路由与Mapping |
|||
|
|||
ABP自动将所有集线器注册到[依赖注入](Dependency-Injection.md)(做为transient)并映射集线器端点. 因此你不需要使用 `app.UseEndpoints(...)` 即可映射你的集线器.集线器路由(URL)通常是根据你的集线器名称确定. |
|||
|
|||
示例: |
|||
|
|||
````csharp |
|||
public class MessagingHub : Hub |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
`MessasingHub` 集线器的路由为 `/signalr-hubs/messasing`: |
|||
|
|||
* 添加了标准 `/signalr-hubs/` 前缀. |
|||
* 使用**驼峰命名**集线器名称,不包含 `Hub` 后缀. |
|||
|
|||
如果你想指定路由,你可以使用 `HubRoute` attribute: |
|||
|
|||
````csharp |
|||
[HubRoute("/my-messasing-hub")] |
|||
public class MessagingHub : Hub |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
### AbpHub 基类 |
|||
|
|||
你可以从 `AbpHub` 或 `AbpHub<T>` 继承标准的 `Hub` 和 `Hub<T>` 类,它们具有实用的基本属性,如 `CurrentUser`. |
|||
|
|||
示例: |
|||
|
|||
````csharp |
|||
public class MessagingHub : AbpHub |
|||
{ |
|||
public async Task SendMessage(string targetUserName, string message) |
|||
{ |
|||
var currentUserName = CurrentUser.UserName; //Access to the current user info |
|||
var txt = L["MyText"]; //Localization |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> 虽然可以将相同的属性注入到集线器构造函数中,但是这种方式简化了集线器类. |
|||
|
|||
### 手动注册/Mapping |
|||
|
|||
ABP会自动将所有集线器注册到[依赖注入](Dependency-Injection.md)作为**transient service**. 如果想要禁用集线器类**自动添加依赖注入**,只需要使用 `DisableConventionalRegistration` attribute. 如果愿意,你仍然可以在模块的 `ConfigureServices` 方法中注册集线器类: |
|||
|
|||
````csharp |
|||
context.Services.AddTransient<MessagingHub>(); |
|||
```` |
|||
|
|||
当**你或ABP**将类注册到依赖注入时,如前几节所述,它会自动映射到端点路由配置. 如果要手动映射集线器类,你可以使用 `DisableAutoHubMap` attribute. |
|||
|
|||
对于手动映射,你有两个选择: |
|||
|
|||
1. 使用 `AbpSignalROptions` 添加map配置(在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中),ABP会为集线器执行端点映射: |
|||
|
|||
````csharp |
|||
Configure<AbpSignalROptions>(options => |
|||
{ |
|||
options.Hubs.Add( |
|||
new HubConfig( |
|||
typeof(MessagingHub), //Hub type |
|||
"/my-messaging/route", //Hub route (URL) |
|||
hubOptions => |
|||
{ |
|||
//Additional options |
|||
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
} |
|||
) |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
这是提供其他SignalR选项的好方式. |
|||
|
|||
如果你不想禁用自动集线器map,但仍想执行其他SignalR配置,可以使用 `options.Hubs.AddOrUpdate(...)` 方法: |
|||
|
|||
````csharp |
|||
Configure<AbpSignalROptions>(options => |
|||
{ |
|||
options.Hubs.AddOrUpdate( |
|||
typeof(MessagingHub), //Hub type |
|||
config => //Additional configuration |
|||
{ |
|||
config.RoutePattern = "/my-messaging-hub"; //override the default route |
|||
config.ConfigureActions.Add(hubOptions => |
|||
{ |
|||
//Additional options |
|||
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
}); |
|||
} |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
你可以通过这种方式修改在依赖模块(没有源代码访问权限)中定义的集线器类的选项. |
|||
|
|||
2. 在[模块](Module-Development-Basics.md)的 `OnApplicationInitialization` 方法中更改 `app.UseConfiguredEndpoints`(添加了lambda方法作为参数). |
|||
|
|||
````csharp |
|||
app.UseConfiguredEndpoints(endpoints => |
|||
{ |
|||
endpoints.MapHub<MessagingHub>("/my-messaging-hub", options => |
|||
{ |
|||
options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
### UserIdProvider |
|||
|
|||
ABP实现 `SignalR` 的 `IUserIdProvider` 接口,从ABP框架的 `ICurrentUser` 服务提供当前用户ID(请参阅[当前用户服务](CurrentUser.md)),它将集成到应用程序的身份验证系统中,实现类是 `AbpSignalRUserIdProvider` (如果你想更改/覆盖它). |
|||
|
|||
## 示例应用程序 |
|||
|
|||
参阅 [SignalR集成Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo),它有一个简单的聊天页面,可以在(经过身份验证的)用户之间发送消息. |
|||
|
|||
 |
|||
@ -0,0 +1,455 @@ |
|||
# 文本模板 |
|||
|
|||
## 介绍 |
|||
|
|||
ABP框架提供了一个简单有效的文本模板系统,文本模板用于动态渲染基于模板和模型(数据对象)内容: |
|||
|
|||
***TEMPLATE + MODEL ==render==> RENDERED CONTENT*** |
|||
|
|||
它非常类似于 ASP.NET Core Razor View (或 Page): |
|||
|
|||
*RAZOR VIEW (or PAGE) + MODEL ==render==> HTML CONTENT* |
|||
|
|||
你可以将渲染的输出用于任何目的,例如发送电子邮件或准备一些报告. |
|||
|
|||
### 示例 |
|||
|
|||
Here, a simple template: |
|||
|
|||
```` |
|||
Hello {%{{{model.name}}}%} :) |
|||
```` |
|||
|
|||
你可以定义一个含有 `Name` 属性的类来渲染这个模板: |
|||
|
|||
````csharp |
|||
public class HelloModel |
|||
{ |
|||
public string Name { get; set; } |
|||
} |
|||
```` |
|||
|
|||
如果你使用 `Name` 为 `John` 的 `HelloModel` 渲染模板,输出为: |
|||
|
|||
```` |
|||
Hello John :) |
|||
```` |
|||
|
|||
模板渲染引擎非常强大; |
|||
|
|||
* 它基于 [Scriban](https://github.com/lunet-io/scriban) 库, 所以它支持 **条件逻辑**, **循环** 等. |
|||
* 模板内容 **可以本地化**. |
|||
* 你可以定义 **布局模板** 在渲染其他模板中用做布局. |
|||
* 对于高级场景,你可以传递任何对象到模板上下文. |
|||
|
|||
### 源码 |
|||
|
|||
这里是本文开发和引用的[示例应用程序源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). |
|||
|
|||
## 安装 |
|||
|
|||
推荐使用 [ABP CLI](CLI.md) 安装包. |
|||
|
|||
### 使用 ABP CLI |
|||
|
|||
在项目目录(.csproj file)打开命令行窗口运行以下命令: |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.TextTemplating |
|||
```` |
|||
|
|||
### 手动安装 |
|||
|
|||
如果你想要手动安装; |
|||
|
|||
1. 添加 [Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) NuGet包到你的项目: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.TextTemplating |
|||
```` |
|||
|
|||
2. 添加 `AbpTextTemplatingModule` 到你的模块依赖列表: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpTextTemplatingModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
## 定义模板 |
|||
|
|||
在渲染模板之前,需要定义它. 创建一个继承自 `TemplateDefinitionProvider` 的类: |
|||
|
|||
````csharp |
|||
public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider |
|||
{ |
|||
public override void Define(ITemplateDefinitionContext context) |
|||
{ |
|||
context.Add( |
|||
new TemplateDefinition("Hello") //template name: "Hello" |
|||
.WithVirtualFilePath( |
|||
"/Demos/Hello/Hello.tpl", //template content path |
|||
isInlineLocalized: true |
|||
) |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `context` 对象用于添加新模板或获取依赖模块定义的模板. 使用 `context.Add(...)` 定义新模板. |
|||
* `TemplateDefinition` 是代表模板的类,每个模板必须有唯一的名称(在渲染模板时使用). |
|||
* `/Demos/Hello/Hello.tpl` 是模板文件的路径. |
|||
* `isInlineLocalized` 声明针对所有语言使用一个模板(`true` 还是针对每种语言使用不同的模板(`false`). 更多内容参阅下面的本地化部分. |
|||
|
|||
### 模板内容 |
|||
|
|||
`WithVirtualFilePath` 表示我们使用[虚拟文件系统](Virtual-File-System.md)存储模板内容. 在项目内创建一个 `Hello.tpl` 文件,并在属性窗口中将其标记为"**嵌入式资源**": |
|||
|
|||
 |
|||
|
|||
示例 `Hello.tpl` 内容如下所示: |
|||
|
|||
```` |
|||
Hello {%{{{model.name}}}%} :) |
|||
```` |
|||
|
|||
[虚拟文件系统](Virtual-File-System.md) 需要在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法添加你的文件: |
|||
|
|||
````csharp |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<TextTemplateDemoModule>("TextTemplateDemo"); |
|||
}); |
|||
```` |
|||
|
|||
* `TextTemplateDemoModule`是模块类. |
|||
* `TextTemplateDemo` 是你的项目的根命名空间. |
|||
|
|||
## 渲染模板 |
|||
|
|||
`ITemplateRenderer` 服务用于渲染模板内容. |
|||
|
|||
### 示例: 渲染一个简单的模板 |
|||
|
|||
````csharp |
|||
public class HelloDemo : ITransientDependency |
|||
{ |
|||
private readonly ITemplateRenderer _templateRenderer; |
|||
|
|||
public HelloDemo(ITemplateRenderer templateRenderer) |
|||
{ |
|||
_templateRenderer = templateRenderer; |
|||
} |
|||
|
|||
public async Task RunAsync() |
|||
{ |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"Hello", //the template name |
|||
new HelloModel |
|||
{ |
|||
Name = "John" |
|||
} |
|||
); |
|||
|
|||
Console.WriteLine(result); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `HelloDemo` 是一个简单的类,在构造函数注入了 `ITemplateRenderer` 并在 `RunAsync` 方法中使用它. |
|||
* `RenderAsync` 有两个基本参数: |
|||
* `templateName`: 要渲染的模板名称 (本示例中是 `Hello`). |
|||
* `model`: 在模板内部用做 `model` 的对象 (本示例中是 `HelloModel` 对象). |
|||
|
|||
示例会返回以下结果: |
|||
|
|||
````csharp |
|||
Hello John :) |
|||
```` |
|||
|
|||
### 匿名模型 |
|||
|
|||
虽然建议为模板创建模型类,但在简单情况下使用匿名对象也是可行的: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"Hello", |
|||
new |
|||
{ |
|||
Name = "John" |
|||
} |
|||
); |
|||
```` |
|||
|
|||
示例中我们并没有创建模型类,但是创建了一个匿名对象模型. |
|||
|
|||
### 大驼峰 与 小驼峰 |
|||
|
|||
PascalCase 属性名(如 `UserName`) 在模板中用做小驼峰(如 `userName`). |
|||
|
|||
## 本地化 |
|||
|
|||
可以基于当前文化对模板内容进行本地化. 以下部分描述了两种类型的本地化选项. |
|||
|
|||
### 内联本地化 |
|||
|
|||
内联本地化使用[本地化系统](Localization.md)本地化模板内的文本. |
|||
|
|||
#### 示例: 重置密码链接 |
|||
|
|||
假设你需要向用户发送电子邮件重置密码. 模板内容: |
|||
|
|||
```` |
|||
<a href="{%{{{model.link}}}%}">{%{{{L "ResetMyPassword"}}}%}</a> |
|||
```` |
|||
|
|||
`L` 函数用于根据当前用户的文化来定位给定的Key,你需要在本地化文件中定义 `ResetMyPassword` 键: |
|||
|
|||
````json |
|||
"ResetMyPassword": "Click here to reset your password" |
|||
```` |
|||
|
|||
你还需要在模板定义提供程序类中声明要与此模板一起使用的本地化资源: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
"PasswordReset", //Template name |
|||
typeof(DemoResource) //LOCALIZATION RESOURCE |
|||
).WithVirtualFilePath( |
|||
"/Demos/PasswordReset/PasswordReset.tpl", //template content path |
|||
isInlineLocalized: true |
|||
) |
|||
); |
|||
```` |
|||
|
|||
当你这样渲染模板时: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"PasswordReset", //the template name |
|||
new PasswordResetModel |
|||
{ |
|||
Link = "https://abp.io/example-link?userId=123&token=ABC" |
|||
} |
|||
); |
|||
```` |
|||
|
|||
你可以看到以下本地化结果: |
|||
|
|||
````csharp |
|||
<a href="https://abp.io/example-link?userId=123&token=ABC">Click here to reset your password</a> |
|||
```` |
|||
|
|||
> 如果你为应用程序定义了 [默认本地化资源](Localization.md), 则无需声明模板定义的资源类型. |
|||
|
|||
### 多个内容本地化 |
|||
|
|||
你可能希望为每种语言创建不同的模板文件,而不是使用本地化系统本地化单个模板. 如果模板对于特定的文化(而不是简单的文本本地化)应该是完全不同的,则可能需要使用它. |
|||
|
|||
#### 示例: 欢迎电子邮件模板 |
|||
|
|||
假设你要发送电子邮件欢迎用户,但要定义基于用户的文化完全不同的模板. |
|||
|
|||
首先创建一个文件夹,将模板放在里面,像 `en.tpl`, `tr.tpl` 每一个你支持的文化: |
|||
|
|||
 |
|||
|
|||
然后在模板定义提供程序类中添加模板定义: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
name: "WelcomeEmail", |
|||
defaultCultureName: "en" |
|||
) |
|||
.WithVirtualFilePath( |
|||
"/Demos/WelcomeEmail/Templates", //template content folder |
|||
isInlineLocalized: false |
|||
) |
|||
); |
|||
```` |
|||
|
|||
* 设置 **默认文化名称**, 当没有所需的文化模板,回退到缺省文化. |
|||
* 指定 **模板文件夹** 而不是单个模板文件. |
|||
* 设置 `isInlineLocalized` 为 `false`. |
|||
|
|||
就这些,你可以渲染当前文化的模板: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync("WelcomeEmail"); |
|||
```` |
|||
|
|||
> 为了简单我们跳过了模型,但是你可以使用前面所述的模型. |
|||
|
|||
### 指定文化 |
|||
|
|||
`ITemplateRenderer` 服务如果没有指定则使用当前文化 (`CultureInfo.CurrentUICulture`). 如果你需要你可以使用 `cultureName` 参数指定文化. |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"WelcomeEmail", |
|||
cultureName: "en" |
|||
); |
|||
```` |
|||
|
|||
## 布局模板 |
|||
|
|||
布局模板用于在其他模板之间创建共享布局. 它类似于ASP.NET Core MVC / Razor Pages中的布局系统. |
|||
|
|||
### 示例: 邮件HTML布局模板 |
|||
|
|||
例如,你想为所有电子邮件模板创建一个布局. |
|||
|
|||
首先像之前一样创建一个模板文件: |
|||
|
|||
````xml |
|||
<!DOCTYPE html> |
|||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
</head> |
|||
<body> |
|||
{%{{{content}}}%} |
|||
</body> |
|||
</html> |
|||
```` |
|||
|
|||
* 布局模板必须具有 **{%{{{content}}}%}** 部分作为渲染的子内容的占位符. |
|||
|
|||
在模板定义提供程序中注册模板: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
"EmailLayout", |
|||
isLayout: true //SET isLayout! |
|||
).WithVirtualFilePath( |
|||
"/Demos/EmailLayout/EmailLayout.tpl", |
|||
isInlineLocalized: true |
|||
) |
|||
); |
|||
```` |
|||
|
|||
现在你可以将此模板用作任何其他模板的布局: |
|||
|
|||
````csharp |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
name: "WelcomeEmail", |
|||
defaultCultureName: "en", |
|||
layout: "EmailLayout" //Set the LAYOUT |
|||
).WithVirtualFilePath( |
|||
"/Demos/WelcomeEmail/Templates", |
|||
isInlineLocalized: false |
|||
) |
|||
); |
|||
```` |
|||
|
|||
## 全局上下文 |
|||
|
|||
ABP传递 `model`,可用于访问模板内的模型. 如果需要,可以传递更多的全局变量. |
|||
|
|||
示例模板内容: |
|||
|
|||
```` |
|||
A global object value: {%{{{myGlobalObject}}}%} |
|||
```` |
|||
|
|||
模板假定它渲染上下文中的 `myGlobalObject` 对象. 你可以如下所示提供它: |
|||
|
|||
````csharp |
|||
var result = await _templateRenderer.RenderAsync( |
|||
"GlobalContextUsage", |
|||
globalContext: new Dictionary<string, object> |
|||
{ |
|||
{"myGlobalObject", "TEST VALUE"} |
|||
} |
|||
); |
|||
```` |
|||
|
|||
渲染的结果将是: |
|||
|
|||
```` |
|||
A global object value: TEST VALUE |
|||
```` |
|||
|
|||
## 高级功能 |
|||
|
|||
本节介绍文本模板系统的一些内部知识和高级用法. |
|||
|
|||
### 模板内容Provider |
|||
|
|||
`TemplateRenderer` 用于渲染模板,这是大多数情况下所需的模板. 但是你可以使用 `ITemplateContentProvider` 获取原始(未渲染的)模板内容. |
|||
|
|||
> `ITemplateRenderer` 内部使用 `ITemplateContentProvider` 获取原始模板内容. |
|||
|
|||
示例: |
|||
|
|||
````csharp |
|||
public class TemplateContentDemo : ITransientDependency |
|||
{ |
|||
private readonly ITemplateContentProvider _templateContentProvider; |
|||
|
|||
public TemplateContentDemo(ITemplateContentProvider templateContentProvider) |
|||
{ |
|||
_templateContentProvider = templateContentProvider; |
|||
} |
|||
|
|||
public async Task RunAsync() |
|||
{ |
|||
var result = await _templateContentProvider |
|||
.GetContentOrNullAsync("Hello"); |
|||
|
|||
Console.WriteLine(result); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
结果是原始模板内容: |
|||
|
|||
```` |
|||
Hello {%{{{model.name}}}%} :) |
|||
```` |
|||
|
|||
* `GetContentOrNullAsync` 如果没有为请求的模板定义任何内容,则返回 `null`. |
|||
* 它可以获取 `cultureName` 参数,如果模板针对不同的文化具有不同的文件,则可以使用该参数(请参见上面的"多内容本地化"部分). |
|||
|
|||
### 模板内容贡献者 |
|||
|
|||
`ITemplateContentProvider` 服务使用 `ITemplateContentContributor` 实现来查找模板内容. 有一个预实现的内容贡献者 `VirtualFileTemplateContentContributor`,它从上面描述的虚拟文件系统中获取模板内容. |
|||
|
|||
你可以实现 `ITemplateContentContributor` 从另一个源读取原始模板内容. |
|||
|
|||
示例: |
|||
|
|||
````csharp |
|||
public class MyTemplateContentProvider |
|||
: ITemplateContentContributor, ITransientDependency |
|||
{ |
|||
public async Task<string> GetOrNullAsync(TemplateContentContributorContext context) |
|||
{ |
|||
var templateName = context.TemplateDefinition.Name; |
|||
|
|||
//TODO: Try to find content from another source |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
如果源无法找到内容, 则返回 `null`, `ITemplateContentProvider` 将回退到下一个贡献者. |
|||
|
|||
### Template Definition Manager |
|||
|
|||
`ITemplateDefinitionManager` 服务可用于获取模板定义(由模板定义提供程序创建). |
|||
|
|||
## 另请参阅 |
|||
|
|||
* 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). |
|||
* [本地化系统](Localization.md). |
|||
* [虚拟文件系统](Virtual-File-System.md). |
|||
@ -1,3 +1,8 @@ |
|||
## Angular 教程 - 第一章 |
|||
# 教程 |
|||
|
|||
TODO... |
|||
## 应用程序开发 |
|||
|
|||
* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [Angular UI](../Part-1?UI=NG) |
|||
|
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
@ -1,478 +1,8 @@ |
|||
## ASP.NET Core MVC 介绍 - 第一章 |
|||
# 教程 |
|||
|
|||
### 关于本教程 |
|||
## 应用程序开发 |
|||
|
|||
在本系列教程中, 你将构建一个用于管理书籍及其作者列表的应用程序. **Entity Framework Core**(EF Core)将用作ORM提供者,因为它是默认数据库提供者. |
|||
* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [Angular UI](../Part-1?UI=NG) |
|||
|
|||
这是本教程所有章节中的第一章,下面是所有的章节: |
|||
|
|||
- **Part I: 创建项目和书籍列表页面(本章)** |
|||
- [Part II: 创建,编辑,删除书籍](Part-II.md) |
|||
- [Part III: 集成测试](Part-III.md) |
|||
|
|||
你可以从[GitHub存储库](https://github.com/abpframework/abp-samples/tree/master/BookStore)访问应用程序的**源代码**. |
|||
|
|||
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). |
|||
|
|||
### 创建项目 |
|||
|
|||
创建一个名为`Acme.BookStore`的新项目, 创建数据库并按照[入门文档](../../../Getting-Started-AspNetCore-MVC-Template.md)运行应用程序. |
|||
|
|||
### 解决方案的结构 |
|||
|
|||
下面的图片展示了从启动模板创建的项目是如何分层的. |
|||
|
|||
 |
|||
|
|||
> 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构.但是,你将通过本教程了解基础知识. |
|||
|
|||
### 创建Book实体 |
|||
|
|||
启动模板中的域层分为两个项目: |
|||
|
|||
- `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象. |
|||
- `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. |
|||
|
|||
在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.Domain.Entities.Auditing; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class Book : AuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public BookType Type { get; set; } |
|||
|
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
|
|||
protected Book() |
|||
{ |
|||
} |
|||
public Book(Guid id, string name, BookType type, DateTime publishDate, float price) |
|||
:base(id) |
|||
{ |
|||
Name = name; |
|||
Type = type; |
|||
PublishDate = publishDate; |
|||
Price = price; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](https://docs.abp.io/zh-Hans/abp/latest/Entities). |
|||
* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). |
|||
* `Guid`是`Book`实体的主键类型. |
|||
* 使用 **数据注解** 为EF Core添加映射.或者你也可以使用 EF Core 自带的[fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling). |
|||
|
|||
#### BookType枚举 |
|||
|
|||
上面所用到的`BookType`枚举定义如下: |
|||
|
|||
````C# |
|||
namespace Acme.BookStore |
|||
{ |
|||
public enum BookType |
|||
{ |
|||
Undefined, |
|||
Adventure, |
|||
Biography, |
|||
Dystopia, |
|||
Fantastic, |
|||
Horror, |
|||
Science, |
|||
ScienceFiction, |
|||
Poetry |
|||
} |
|||
} |
|||
```` |
|||
|
|||
#### 将Book实体添加到DbContext中 |
|||
|
|||
EF Core需要你将实体和DbContext建立关联.最简单的做法是在`Acme.BookStore.EntityFrameworkCore`项目的`BookStoreDbContext`类中添加`DbSet`属性.如下所示: |
|||
|
|||
````C# |
|||
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext> |
|||
{ |
|||
public DbSet<Book> Books { get; set; } |
|||
... |
|||
} |
|||
```` |
|||
|
|||
#### 配置你的Book实体 |
|||
|
|||
在`Acme.BookStore.EntityFrameworkCore`项目中打开`BookStoreDbContextModelCreatingExtensions.cs`文件,并将以下代码添加到`ConfigureBookStore`方法的末尾以配置Book实体: |
|||
|
|||
````C# |
|||
builder.Entity<Book>(b => |
|||
{ |
|||
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); |
|||
b.ConfigureByConvention(); //auto configure for the base class props |
|||
b.Property(x => x.Name).IsRequired().HasMaxLength(128); |
|||
}); |
|||
```` |
|||
|
|||
#### 添加新的Migration并更新数据库 |
|||
|
|||
这个启动模板使用了[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)来创建并维护数据库结构.打开 **程序包管理器控制台(Package Manager Console) (PMC)** (工具/Nuget包管理器菜单),选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations`作为默认的项目然后执行下面的命令: |
|||
|
|||
 |
|||
|
|||
这样就会在`Migrations`文件夹中创建一个新的migration类.然后执行`Update-Database`命令更新数据库结构. |
|||
|
|||
```` |
|||
PM> Update-Database |
|||
```` |
|||
|
|||
#### 添加示例数据 |
|||
|
|||
`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们: |
|||
|
|||
 |
|||
|
|||
### 创建应用服务 |
|||
|
|||
下一步是创建[应用服务](../../../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目: |
|||
|
|||
* `Acme.BookStore.Application.Contracts`主要包含你的DTO和应用程序服务接口. |
|||
* `Acme.BookStore.Application`包含应用程序服务的实现. |
|||
|
|||
#### BookDto |
|||
|
|||
在`Acme.BookStore.Application.Contracts`项目中创建一个名为`BookDto`的DTO类: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookDto : AuditedEntityDto<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public BookType Type { get; set; } |
|||
|
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects)查看更多信息. |
|||
* 为了在页面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层. |
|||
* `BookDto`继承自 `AuditedEntityDto<Guid>`.跟上面定义的`Book`类一样具有一些审计属性. |
|||
|
|||
在将书籍返回到表示层时,需要将`Book`实体转换为`BookDto`对象. [AutoMapper](https://automapper.org)库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在`Acme.BookStore.Application`项目的`BookStoreApplicationAutoMapperProfile`类中定义映射: |
|||
|
|||
````csharp |
|||
using AutoMapper; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreApplicationAutoMapperProfile : Profile |
|||
{ |
|||
public BookStoreApplicationAutoMapperProfile() |
|||
{ |
|||
CreateMap<Book, BookDto>(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
#### CreateUpdateBookDto |
|||
|
|||
在`Acme.BookStore.Application.Contracts`项目中创建一个名为`CreateUpdateBookDto`的DTO类: |
|||
|
|||
````c# |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.AutoMapper; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class CreateUpdateBookDto |
|||
{ |
|||
[Required] |
|||
[StringLength(128)] |
|||
public string Name { get; set; } |
|||
|
|||
[Required] |
|||
public BookType Type { get; set; } = BookType.Undefined; |
|||
|
|||
[Required] |
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
[Required] |
|||
public float Price { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息. |
|||
* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation). |
|||
|
|||
就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射: |
|||
|
|||
````csharp |
|||
CreateMap<CreateUpdateBookDto, Book>(); |
|||
```` |
|||
|
|||
#### IBookAppService |
|||
|
|||
在`Acme.BookStore.Application.Contracts`项目中定义一个名为`IBookAppService`的接口: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public interface IBookAppService : |
|||
ICrudAppService< //定义了CRUD方法 |
|||
BookDto, //用来展示书籍 |
|||
Guid, //Book实体的主键 |
|||
PagedAndSortedResultRequestDto, //获取书籍的时候用于分页和排序 |
|||
CreateUpdateBookDto, //用于创建书籍 |
|||
CreateUpdateBookDto> //用于更新书籍 |
|||
{ |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 框架定义应用程序服务的接口<u>不是必需的</u>. 但是,它被建议作为最佳实践. |
|||
* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 你可以从空的`IApplicationService`接口继承并手动定义自己的方法. |
|||
* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定. |
|||
|
|||
|
|||
#### BookAppService |
|||
|
|||
在`Acme.BookStore.Application`项目中实现名为`BookAppService`的`IBookAppService`: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookAppService : |
|||
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, |
|||
CreateUpdateBookDto, CreateUpdateBookDto>, |
|||
IBookAppService |
|||
{ |
|||
public BookAppService(IRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `BookAppService`继承了`CrudAppService<...>`.它实现了上面定义的CRUD方法. |
|||
* `BookAppService`注入`IRepository <Book,Guid>`,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](https://docs.abp.io/zh-Hans/abp/latest/Repositories) |
|||
* `BookAppService`使用`IObjectMapper`将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作. |
|||
|
|||
### 自动生成API Controllers |
|||
|
|||
你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](https://docs.abp.io/zh-Hans/abp/latest/API/Auto-API-Controllers)按照惯例将你的应用程序服务配置为MVC API控制器. |
|||
|
|||
#### Swagger UI |
|||
|
|||
启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL. |
|||
|
|||
你会看到一些内置的接口和`Book`的接口,它们都是REST风格的: |
|||
|
|||
 |
|||
|
|||
Swagger有一个很好的UI来测试API. 你可以尝试执行`[GET] /api/app/book` API来获取书籍列表. |
|||
|
|||
### 动态JavaScript代理 |
|||
|
|||
在Javascript端通过AJAX的方式调用HTTP API接口是很常见的,你可以使用`$.ajax`或者其他的工具来调用接口.当然,ABP中提供了更好的方式. |
|||
|
|||
ABP **自动** 为所有的API接口创建了JavaScript **代理**.因此,你可以像调用 **JavaScript function**一样调用任何接口. |
|||
|
|||
#### 在浏览器的开发者控制台中测试接口 |
|||
|
|||
你可以使用你钟爱的浏览器的 **开发者控制台** 中轻松测试JavaScript代理.运行程序,并打开浏览器的 **开发者工具**(快捷键:F12),切换到 **Console** 标签,输入下面的代码并回车: |
|||
|
|||
````js |
|||
acme.bookStore.book.getList({}).done(function (result) { console.log(result); }); |
|||
```` |
|||
|
|||
* `acme.bookStore`是`BookAppService`的命名空间,转换成了[驼峰命名](https://en.wikipedia.org/wiki/Camel_case). |
|||
* `book`是`BookAppService`转换后的名字(去除了AppService后缀并转成了驼峰命名). |
|||
* `getList`是定义在`AsyncCrudAppService`基类中的`GetListAsync`方法转换后的名字(去除了Async后缀并转成了驼峰命名). |
|||
* `{}`参数用于将空对象发送到`GetListAsync`方法,该方法通常需要一个类型为`PagedAndSortedResultRequestDto`的对象,用于向服务器发送分页和排序选项(所有属性都是可选的,所以你可以发送一个空对象). |
|||
* `getList`方法返回了一个`promise`.因此,你可以传递一个回调函数到`done`(或者`then`)方法中来获取服务返回的结果. |
|||
|
|||
运行这段代码会产生下面的输出: |
|||
|
|||
 |
|||
|
|||
你可以看到服务器返回的 **book list**.你还可以切换到开发者工具的 **network** 查看客户端到服务器端的通讯信息: |
|||
|
|||
 |
|||
|
|||
我们使用`create`方法 **创建一本新书**: |
|||
|
|||
````js |
|||
acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); }); |
|||
```` |
|||
|
|||
你会看到控制台会显示类似这样的输出: |
|||
|
|||
```` |
|||
successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7 |
|||
```` |
|||
|
|||
检查数据库中的`Books`表以查看新书. 你可以自己尝试`get`,`update`和`delete`功能. |
|||
|
|||
### 创建书籍页面 |
|||
|
|||
现在我们来创建一些可见和可用的东西,取代经典的MVC,我们使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start). |
|||
|
|||
|
|||
在 `Acme.BookStore.Web`项目的`Pages`文件夹下创建一个新的文件夹叫`Books`并添加一个名为`Index.cshtml`的Razor Page. |
|||
|
|||
 |
|||
|
|||
打开`Index.cshtml`并把内容修改成下面这样: |
|||
|
|||
````html |
|||
@page |
|||
@using Acme.BookStore.Web.Pages.Books |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@model IndexModel |
|||
|
|||
<h2>Books</h2> |
|||
```` |
|||
|
|||
* 此代码更改了Razor View Page Model的默认继承,因此它从`BookStorePage`类(而不是`PageModel`)继承.启动模板附带的`BookStorePage`类,提供所有页面使用的一些共享属性/方法. |
|||
* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Books`命名空间,或者在`Index.cshtml`中更新它. |
|||
|
|||
#### 将Books页面添加到主菜单 |
|||
|
|||
打开`Menus`文件夹中的 `BookStoreMenuContributor` 类,在`ConfigureMainMenuAsync`方法的底部添加如下代码: |
|||
|
|||
````c# |
|||
context.Menu.AddItem( |
|||
new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) |
|||
.AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books")) |
|||
); |
|||
```` |
|||
|
|||
#### 本地化菜单 |
|||
|
|||
本地化文本位于`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下: |
|||
|
|||
 |
|||
|
|||
打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾: |
|||
|
|||
````json |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Menu:BookStore": "Book Store", |
|||
"Menu:Books": "Books" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](https://docs.abp.io/zh-Hans/abp/latest/Localization). |
|||
* 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加`Menu:`前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将**返回**到本地化的key(ASP.NET Core的标准行为). |
|||
|
|||
运行该应用程序,看到新菜单项已添加到顶部栏: |
|||
|
|||
 |
|||
|
|||
点击Books菜单项就会跳转到新增的书籍页面. |
|||
|
|||
#### 书籍列表 |
|||
|
|||
我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示页面上的表格列表. 数据表可以完全通过AJAX工作,速度快,并提供良好的用户体验. Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,而需要在页面中引用样式和脚本文件. |
|||
|
|||
##### Index.cshtml |
|||
|
|||
将`Pages/Books/Index.cshtml`改成下面的样子: |
|||
|
|||
````html |
|||
@page |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@model Acme.BookStore.Web.Pages.Books.IndexModel |
|||
@section scripts |
|||
{ |
|||
<abp-script src="/Pages/Books/index.js" /> |
|||
} |
|||
<abp-card> |
|||
<abp-card-header> |
|||
<h2>@L["Books"]</h2> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<abp-table striped-rows="true" id="BooksTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>@L["Name"]</th> |
|||
<th>@L["Type"]</th> |
|||
<th>@L["PublishDate"]</th> |
|||
<th>@L["Price"]</th> |
|||
<th>@L["CreationTime"]</th> |
|||
</tr> |
|||
</thead> |
|||
</abp-table> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
```` |
|||
|
|||
* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Bundling-Minification)获取更多信息. |
|||
* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Tag-Helpers/Index). |
|||
* 你可以像上面本地化菜单一样 **本地化** 列名. |
|||
|
|||
#### 添加脚本文件 |
|||
|
|||
在`Pages/Books/`文件夹中创建 `index.js`文件 |
|||
|
|||
 |
|||
|
|||
`index.js`的内容如下: |
|||
|
|||
````js |
|||
$(function () { |
|||
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ |
|||
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), |
|||
columnDefs: [ |
|||
{ data: "name" }, |
|||
{ data: "type" }, |
|||
{ data: "publishDate" }, |
|||
{ data: "price" }, |
|||
{ data: "creationTime" } |
|||
] |
|||
})); |
|||
}); |
|||
```` |
|||
|
|||
* `abp.libs.datatables.createAjax`是帮助ABP的动态JavaScript API代理跟Datatable的格式相适应的辅助方法. |
|||
* `abp.libs.datatables.normalizeConfiguration`是另一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置. |
|||
* `acme.bookStore.book.getList`是获取书籍列表的方法(上面已经介绍过了) |
|||
* 查看 [Datatable文档](https://datatables.net/manual/) 了解更多配置项. |
|||
|
|||
最终的页面如下: |
|||
|
|||
 |
|||
|
|||
### 下一章 |
|||
|
|||
点击查看 [下一章](Part-II.md) 的介绍. |
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
@ -1,432 +1,8 @@ |
|||
## ASP.NET Core MVC 教程 - 第二章 |
|||
# 教程 |
|||
|
|||
### 关于本教程 |
|||
## 应用程序开发 |
|||
|
|||
这是ASP.NET Core MVC教程系列的第二章. 查看其它章节 |
|||
* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [Angular UI](../Part-1?UI=NG) |
|||
|
|||
* [Part I: 创建项目和书籍列表页面](Part-I.md) |
|||
* **Part II: 创建,编辑,删除书籍(本章)** |
|||
* [Part III: 集成测试](Part-III.md) |
|||
|
|||
你可以从[GitHub存储库](https://github.com/volosoft/abp/tree/master/samples/BookStore)访问应用程序的**源代码**. |
|||
|
|||
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). |
|||
|
|||
### 新增 Book 实体 |
|||
|
|||
通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: |
|||
|
|||
 |
|||
|
|||
#### 新建 modal form |
|||
|
|||
在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: |
|||
|
|||
 |
|||
|
|||
##### CreateModal.cshtml.cs |
|||
|
|||
展开 `CreateModal.cshtml`,打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现: |
|||
|
|||
````C# |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
|
|||
namespace Acme.BookStore.Web.Pages.Books |
|||
{ |
|||
public class CreateModalModel : BookStorePageModel |
|||
{ |
|||
[BindProperty] |
|||
public CreateUpdateBookDto Book { get; set; } |
|||
|
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public CreateModalModel(IBookAppService bookAppService) |
|||
{ |
|||
_bookAppService = bookAppService; |
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await _bookAppService.CreateAsync(Book); |
|||
return NoContent(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法. |
|||
* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上. |
|||
* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法. |
|||
|
|||
##### CreateModal.cshtml |
|||
|
|||
打开 `CreateModal.cshtml` 文件并粘贴如下代码: |
|||
|
|||
````html |
|||
@page |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal |
|||
@model Acme.BookStore.Web.Pages.Books.CreateModalModel |
|||
@{ |
|||
Layout = null; |
|||
} |
|||
<abp-dynamic-form abp-model="Book" data-ajaxForm="true" asp-page="/Books/CreateModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["NewBook"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
```` |
|||
|
|||
* 这个 modal 使用 `abp-dynamic-form` Tag Helper 根据 `CreateBookViewModel` 类自动构建了表单. |
|||
* `abp-model` 指定了 `Book` 属性为模型对象. |
|||
* `data-ajaxForm` 设置了表单通过AJAX提交,而不是经典的页面回发. |
|||
* `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要). |
|||
|
|||
#### 添加 "New book" 按钮 |
|||
|
|||
打开 `Pages/Books/Index.cshtml` 并按如下代码修改 `abp-card-header` : |
|||
|
|||
````html |
|||
<abp-card-header> |
|||
<abp-row> |
|||
<abp-column size-md="_6"> |
|||
<h2>@L["Books"]</h2> |
|||
</abp-column> |
|||
<abp-column size-md="_6" class="text-right"> |
|||
<abp-button id="NewBookButton" |
|||
text="@L["NewBook"].Value" |
|||
icon="plus" |
|||
button-type="Primary" /> |
|||
</abp-column> |
|||
</abp-row> |
|||
</abp-card-header> |
|||
```` |
|||
|
|||
如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: |
|||
|
|||
 |
|||
|
|||
打开 `Pages/books/index.js` 在datatable配置代码后面添加如下代码: |
|||
|
|||
````js |
|||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
```` |
|||
|
|||
* `abp.ModalManager` 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节. |
|||
|
|||
现在,你可以 **运行程序** 通过新的 modal form 来创建书籍了. |
|||
|
|||
### 编辑更新已存在的 Book 实体 |
|||
|
|||
在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: |
|||
|
|||
 |
|||
|
|||
#### EditModal.cshtml.cs |
|||
|
|||
展开 `EditModal.cshtml`,打开 `EditModal.cshtml.cs` 文件( `EditModalModel` 类) 并替换成以下代码: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
|
|||
namespace Acme.BookStore.Web.Pages.Books |
|||
{ |
|||
public class EditModalModel : BookStorePageModel |
|||
{ |
|||
[HiddenInput] |
|||
[BindProperty(SupportsGet = true)] |
|||
public Guid Id { get; set; } |
|||
|
|||
[BindProperty] |
|||
public CreateUpdateBookDto Book { get; set; } |
|||
|
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public EditModalModel(IBookAppService bookAppService) |
|||
{ |
|||
_bookAppService = bookAppService; |
|||
} |
|||
|
|||
public async Task OnGetAsync() |
|||
{ |
|||
var bookDto = await _bookAppService.GetAsync(Id); |
|||
Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto); |
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await _bookAppService.UpdateAsync(Id, Book); |
|||
return NoContent(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `[HiddenInput]` 和 `[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串中获取Id的值. |
|||
* 在 `OnGetAsync` 方法中,将 `BookAppService.GetAsync` 方法返回的 `BookDto` 映射成 `CreateUpdateBookDto` 并赋值给Book属性. |
|||
* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体. |
|||
|
|||
#### BookDto到CreateUpdateBookDto对象映射 |
|||
|
|||
为了执行`BookDto`到`CreateUpdateBookDto`对象映射,请打开`Acme.BookStore.Web`项目中的`BookStoreWebAutoMapperProfile.cs`并更改它,如下所示: |
|||
|
|||
````csharp |
|||
using AutoMapper; |
|||
|
|||
namespace Acme.BookStore.Web |
|||
{ |
|||
public class BookStoreWebAutoMapperProfile : Profile |
|||
{ |
|||
public BookStoreWebAutoMapperProfile() |
|||
{ |
|||
CreateMap<BookDto, CreateUpdateBookDto>(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 刚刚添加了`CreateMap<BookDto, CreateUpdateBookDto>();`作为映射定义. |
|||
|
|||
#### EditModal.cshtml |
|||
|
|||
将 `EditModal.cshtml` 页面内容替换成如下代码: |
|||
|
|||
````html |
|||
@page |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@using Acme.BookStore.Web.Pages.Books |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal |
|||
@model EditModalModel |
|||
@{ |
|||
Layout = null; |
|||
} |
|||
<abp-dynamic-form abp-model="Book" data-ajaxForm="true" asp-page="/Books/EditModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["Update"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-input asp-for="Id" /> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
```` |
|||
|
|||
这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点: |
|||
|
|||
* 它包含`id`属性的`abp-input`, 用于存储编辑书的id(它是隐藏的Input) |
|||
* 此页面指定的post地址是`Books/EditModal`, 并用文本 *Update* 作为 modal 标题. |
|||
|
|||
#### 为表格添加 "操作(Actions)" 下拉菜单 |
|||
|
|||
我们将为表格每行添加下拉按钮 ("Actions") . 最终效果如下: |
|||
|
|||
 |
|||
|
|||
打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码: |
|||
|
|||
````html |
|||
<abp-table striped-rows="true" id="BooksTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>@L["Actions"]</th> |
|||
<th>@L["Name"]</th> |
|||
<th>@L["Type"]</th> |
|||
<th>@L["PublishDate"]</th> |
|||
<th>@L["Price"]</th> |
|||
<th>@L["CreationTime"]</th> |
|||
</tr> |
|||
</thead> |
|||
</abp-table> |
|||
```` |
|||
|
|||
* 只是为"Actions"增加了一个 `th` 标签. |
|||
|
|||
打开 `Pages/books/index.js` 并用以下内容进行替换: |
|||
|
|||
````js |
|||
$(function () { |
|||
|
|||
var l = abp.localization.getResource('BookStore'); |
|||
|
|||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); |
|||
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); |
|||
|
|||
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ |
|||
processing: true, |
|||
serverSide: true, |
|||
paging: true, |
|||
searching: false, |
|||
autoWidth: false, |
|||
scrollCollapse: true, |
|||
order: [[1, "asc"]], |
|||
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), |
|||
columnDefs: [ |
|||
{ |
|||
rowAction: { |
|||
items: |
|||
[ |
|||
{ |
|||
text: l('Edit'), |
|||
action: function (data) { |
|||
editModal.open({ id: data.record.id }); |
|||
} |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ data: "name" }, |
|||
{ data: "type" }, |
|||
{ data: "publishDate" }, |
|||
{ data: "price" }, |
|||
{ data: "creationTime" } |
|||
] |
|||
})); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
editModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
* 通过 `abp.localization.getResource('BookStore')` 可以在客户端使用服务器端定义的相同的本地化语言文本. |
|||
* 添加了一个名为 `createModal` 的新的 `ModalManager` 来打开创建用的 modal 对话框. |
|||
* 添加了一个名为 `editModal` 的新的 `ModalManager` 来打开编辑用的 modal 对话框. |
|||
* 在 `columnDefs` 起始处新增一列用于显示 "Actions" 下拉按钮. |
|||
* "New Book"动作只需调用`createModal.open`来打开创建对话框. |
|||
* "Edit" 操作只是简单调用 `editModal.open` 来打开编辑对话框. |
|||
|
|||
现在,你可以运行程序,通过编辑操作来更新任一个book实体. |
|||
|
|||
### 删除一个已有的Book实体 |
|||
|
|||
打开 `Pages/books/index.js` 文件,在 `rowAction` `items` 下新增一项: |
|||
|
|||
````js |
|||
{ |
|||
text: l('Delete'), |
|||
confirmMessage: function (data) { |
|||
return l('BookDeletionConfirmationMessage', data.record.name); |
|||
}, |
|||
action: function (data) { |
|||
acme.bookStore.book |
|||
.delete(data.record.id) |
|||
.then(function() { |
|||
abp.notify.info(l('SuccessfullyDeleted')); |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `confirmMessage` 用来在实际执行 `action` 之前向用户进行确认. |
|||
* 通过javascript代理方法 `acme.bookStore.book.delete` 执行一个AJAX请求来删除一个book实体. |
|||
* `abp.notify.info` 用来在执行删除操作后显示一个toastr通知信息. |
|||
|
|||
最终的 `index.js` 文件内容如下所示: |
|||
|
|||
````js |
|||
$(function () { |
|||
|
|||
var l = abp.localization.getResource('BookStore'); |
|||
|
|||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); |
|||
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); |
|||
|
|||
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ |
|||
processing: true, |
|||
serverSide: true, |
|||
paging: true, |
|||
searching: false, |
|||
autoWidth: false, |
|||
scrollCollapse: true, |
|||
order: [[1, "asc"]], |
|||
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), |
|||
columnDefs: [ |
|||
{ |
|||
rowAction: { |
|||
items: |
|||
[ |
|||
{ |
|||
text: l('Edit'), |
|||
action: function (data) { |
|||
editModal.open({ id: data.record.id }); |
|||
} |
|||
}, |
|||
{ |
|||
text: l('Delete'), |
|||
confirmMessage: function (data) { |
|||
return l('BookDeletionConfirmationMessage', data.record.name); |
|||
}, |
|||
action: function (data) { |
|||
acme.bookStore.book |
|||
.delete(data.record.id) |
|||
.then(function() { |
|||
abp.notify.info(l('SuccessfullyDeleted')); |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
} |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ data: "name" }, |
|||
{ data: "type" }, |
|||
{ data: "publishDate" }, |
|||
{ data: "price" }, |
|||
{ data: "creationTime" } |
|||
] |
|||
})); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
editModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
打开`Acme.BookStore.Domain.Shared`项目中的`en.json`并添加以下行: |
|||
|
|||
````json |
|||
"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", |
|||
"SuccessfullyDeleted": "Successfully deleted" |
|||
```` |
|||
|
|||
运行程序并尝试删除一个book实体. |
|||
|
|||
### 下一章 |
|||
|
|||
查看本教程的 [下一章](Part-III.md) . |
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
@ -1,165 +1,8 @@ |
|||
## ASP.NET Core MVC 教程 - 第三章 |
|||
# 教程 |
|||
|
|||
### 关于本教程 |
|||
## 应用程序开发 |
|||
|
|||
这是ASP.NET Core MVC教程系列的第三章. 查看其它章节 |
|||
* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [Angular UI](../Part-1?UI=NG) |
|||
|
|||
- [Part I: 创建项目和书籍列表页面](Part-I.md) |
|||
- [Part II: 创建,编辑,删除书籍](Part-II.md) |
|||
- **Part III: 集成测试(本章)** |
|||
|
|||
你可以从[GitHub存储库](https://github.com/volosoft/abp/tree/master/samples/BookStore)访问应用程序的**源代码**. |
|||
|
|||
> 你也可以观看由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`类如下所示: |
|||
|
|||
````C# |
|||
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(_guidGenerator.Create(), "Test book 1", |
|||
BookType.Fantastic, new DateTime(2015, 05, 24), 21)); |
|||
|
|||
await _bookRepository.InsertAsync( |
|||
new Book(_guidGenerator.Create(), "Test book 2", |
|||
BookType.Science, new DateTime(2014, 02, 11), 15)); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 注入`IRepository<Book,Guid>`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. |
|||
* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息). |
|||
|
|||
### 测试 BookAppService |
|||
|
|||
在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: |
|||
|
|||
````C# |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Xunit; |
|||
|
|||
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实体失败的场景: |
|||
|
|||
````C# |
|||
[Fact] |
|||
public async Task Should_Not_Create_A_Book_Without_Name() |
|||
{ |
|||
var exception = await Assert.ThrowsAsync<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 -> 测试资源管理器)并**执行**所有测试: |
|||
|
|||
 |
|||
|
|||
恭喜, 绿色图标表示测试已成功通过! |
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 45 KiB |
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
|||
{ |
|||
[Serializable] |
|||
public class ExtensionEnumDto |
|||
{ |
|||
public List<ExtensionEnumFieldDto> Fields { get; set; } |
|||
|
|||
public string LocalizationResource { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
|||
{ |
|||
[Serializable] |
|||
public class ExtensionEnumFieldDto |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public object Value { get; set; } |
|||
} |
|||
} |
|||
@ -1,36 +1,13 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending |
|||
{ |
|||
[Serializable] |
|||
public class ExtensionPropertyAttributeDto |
|||
{ |
|||
public string Type { get; set; } |
|||
public string TypeSimple { get; set; } |
|||
public Dictionary<string, object> Configuration { get; set; } |
|||
|
|||
public static ExtensionPropertyAttributeDto Create(Attribute attribute) |
|||
{ |
|||
var attributeType = attribute.GetType(); |
|||
var dto = new ExtensionPropertyAttributeDto |
|||
{ |
|||
Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(attributeType), |
|||
TypeSimple = TypeHelper.GetSimplifiedName(attributeType), |
|||
Configuration = new Dictionary<string, object>() |
|||
}; |
|||
|
|||
if (attribute is StringLengthAttribute stringLengthAttribute) |
|||
{ |
|||
dto.Configuration["MaximumLength"] = stringLengthAttribute.MaximumLength; |
|||
dto.Configuration["MinimumLength"] = stringLengthAttribute.MinimumLength; |
|||
} |
|||
|
|||
//TODO: Others!
|
|||
|
|||
return dto; |
|||
} |
|||
public Dictionary<string, object> Config { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
{ |
|||
"culture": "ar", |
|||
"texts": { |
|||
"GivenTenantIsNotAvailable": "الجهة المحددة غير متاحة: {0}", |
|||
"Tenant": "الجهة", |
|||
"Switch": "تغيير", |
|||
"Name": "اسم", |
|||
"SwitchTenantHint": "اترك حقل الاسم فارغًا للتبديل إلى المضيف.", |
|||
"SwitchTenant": "تغيير الجهة", |
|||
"NotSelected": "غير محدد" |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JsTree |
|||
{ |
|||
[DependsOn(typeof(JQueryScriptContributor))] |
|||
public class JsTreeScriptContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/jstree/jstree.min.js"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JsTree |
|||
{ |
|||
public class JsTreeOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Path of the style file for the JsTree library.
|
|||
/// Setting to null ignores the style file.
|
|||
///
|
|||
/// Default value: "/libs/jstree/themes/default/style.min.css".
|
|||
/// </summary>
|
|||
public string StylePath { get; set; } = "/libs/jstree/themes/default/style.min.css"; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JsTree |
|||
{ |
|||
public class JsTreeStyleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
var options = context |
|||
.ServiceProvider |
|||
.GetRequiredService<IOptions<JsTreeOptions>>() |
|||
.Value; |
|||
|
|||
if (options.StylePath.IsNullOrEmpty()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
context.Files.AddIfNotContains(options.StylePath); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Core; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR |
|||
{ |
|||
[DependsOn(typeof(CoreScriptContributor))] |
|||
public class SignalRBrowserScriptContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/signalr/browser/signalr.js"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,150 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace Volo.Abp.ObjectExtending |
|||
{ |
|||
public static class MvcUiObjectExtensionPropertyInfoExtensions |
|||
{ |
|||
private static readonly HashSet<Type> NumberTypes = new HashSet<Type> { |
|||
typeof(int), |
|||
typeof(long), |
|||
typeof(byte), |
|||
typeof(sbyte), |
|||
typeof(short), |
|||
typeof(ushort), |
|||
typeof(uint), |
|||
typeof(long), |
|||
typeof(ulong), |
|||
typeof(float), |
|||
typeof(double), |
|||
typeof(decimal), |
|||
typeof(int?), |
|||
typeof(long?), |
|||
typeof(byte?), |
|||
typeof(sbyte?), |
|||
typeof(short?), |
|||
typeof(ushort?), |
|||
typeof(uint?), |
|||
typeof(long?), |
|||
typeof(ulong?), |
|||
typeof(float?), |
|||
typeof(double?), |
|||
typeof(decimal?) |
|||
}; |
|||
|
|||
public static string GetInputFormatOrNull(this IBasicObjectExtensionPropertyInfo property) |
|||
{ |
|||
if (property.IsDate()) |
|||
{ |
|||
return "{0:yyyy-MM-dd}"; |
|||
} |
|||
|
|||
if (property.IsDateTime()) |
|||
{ |
|||
return "{0:yyyy-MM-ddTHH:mm}"; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public static string GetInputValueOrNull(this IBasicObjectExtensionPropertyInfo property, object value) |
|||
{ |
|||
if (value == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (TypeHelper.IsFloatingType(property.Type)) |
|||
{ |
|||
return value.ToString()?.Replace(',', '.'); |
|||
} |
|||
|
|||
/* Let the ASP.NET Core handle it! */ |
|||
return null; |
|||
} |
|||
|
|||
public static string GetInputType(this ObjectExtensionPropertyInfo propertyInfo) |
|||
{ |
|||
foreach (var attribute in propertyInfo.Attributes) |
|||
{ |
|||
var inputTypeByAttribute = GetInputTypeFromAttributeOrNull(attribute); |
|||
if (inputTypeByAttribute != null) |
|||
{ |
|||
return inputTypeByAttribute; |
|||
} |
|||
} |
|||
|
|||
return GetInputTypeFromTypeOrNull(propertyInfo.Type) |
|||
?? "text"; //default
|
|||
} |
|||
|
|||
private static string GetInputTypeFromAttributeOrNull(Attribute attribute) |
|||
{ |
|||
if (attribute is EmailAddressAttribute) |
|||
{ |
|||
return "email"; |
|||
} |
|||
|
|||
if (attribute is UrlAttribute) |
|||
{ |
|||
return "url"; |
|||
} |
|||
|
|||
if (attribute is HiddenInputAttribute) |
|||
{ |
|||
return "hidden"; |
|||
} |
|||
|
|||
if (attribute is PhoneAttribute) |
|||
{ |
|||
return "tel"; |
|||
} |
|||
|
|||
if (attribute is DataTypeAttribute dataTypeAttribute) |
|||
{ |
|||
switch (dataTypeAttribute.DataType) |
|||
{ |
|||
case DataType.Password: |
|||
return "password"; |
|||
case DataType.Date: |
|||
return "date"; |
|||
case DataType.Time: |
|||
return "time"; |
|||
case DataType.EmailAddress: |
|||
return "email"; |
|||
case DataType.Url: |
|||
return "url"; |
|||
case DataType.PhoneNumber: |
|||
return "tel"; |
|||
case DataType.DateTime: |
|||
return "datetime-local"; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private static string GetInputTypeFromTypeOrNull(Type type) |
|||
{ |
|||
if (type == typeof(bool)) |
|||
{ |
|||
return "checkbox"; |
|||
} |
|||
|
|||
if (type == typeof(DateTime)) |
|||
{ |
|||
return "datetime-local"; |
|||
} |
|||
|
|||
if (NumberTypes.Contains(type)) |
|||
{ |
|||
return "number"; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||