mirror of https://github.com/abpframework/abp.git
62 changed files with 773 additions and 618 deletions
@ -0,0 +1,227 @@ |
|||
# 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 that 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.BackgroundJobs.HangFire |
|||
``` |
|||
|
|||
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. |
|||
|
|||
 |
|||
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 45 KiB |
@ -0,0 +1,56 @@ |
|||
# 示例应用 |
|||
|
|||
这些是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) |
|||
* **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) |
|||
* **自定义认证**: 如何为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) |
|||
@ -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,14 +1,12 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public class AbpSignalROptions |
|||
{ |
|||
public List<HubConfig> Hubs { get; } |
|||
public HubConfigList Hubs { get; } |
|||
|
|||
public AbpSignalROptions() |
|||
{ |
|||
Hubs = new List<HubConfig>(); |
|||
Hubs = new HubConfigList(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public class DisableAutoHubMapAttribute : Attribute |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public class HubConfigList : List<HubConfig> |
|||
{ |
|||
public void AddOrUpdate<THub>(Action<HubConfig> configAction = null) |
|||
{ |
|||
AddOrUpdate(typeof(THub)); |
|||
} |
|||
|
|||
public void AddOrUpdate(Type hubType, Action<HubConfig> configAction = null) |
|||
{ |
|||
var hubConfig = this.GetOrAdd( |
|||
c => c.HubType == hubType, |
|||
() => HubConfig.Create(hubType) |
|||
); |
|||
|
|||
configAction?.Invoke(hubConfig); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
{ |
|||
"culture": "es", |
|||
"texts": { |
|||
"DisplayName:Abp.Mailing.DefaultFromAddress": "Dirección de envío", |
|||
"DisplayName:Abp.Mailing.DefaultFromDisplayName": "Nombre de envío", |
|||
"DisplayName:Abp.Mailing.Smtp.Host": "Host", |
|||
"DisplayName:Abp.Mailing.Smtp.Port": "Puerto", |
|||
"DisplayName:Abp.Mailing.Smtp.UserName": "Nombre de usuario", |
|||
"DisplayName:Abp.Mailing.Smtp.Password": "Contraseña", |
|||
"DisplayName:Abp.Mailing.Smtp.Domain": "Dominio", |
|||
"DisplayName:Abp.Mailing.Smtp.EnableSsl": "Habilitar SSL", |
|||
"DisplayName:Abp.Mailing.Smtp.UseDefaultCredentials": "Usar las credenciales por defecto", |
|||
"Description:Abp.Mailing.DefaultFromAddress": "La dirección de envío por defecto", |
|||
"Description:Abp.Mailing.DefaultFromDisplayName": "El nombre de envío por defecto", |
|||
"Description:Abp.Mailing.Smtp.Host": "El nombre o dirección del host para transacciones SMTP.", |
|||
"Description:Abp.Mailing.Smtp.Port": "El puerto usado para transacciones SMTP.", |
|||
"Description:Abp.Mailing.Smtp.UserName": "Nombre de usuario asociado a las credenciales.", |
|||
"Description:Abp.Mailing.Smtp.Password": "La contraseña del ususario asociado a las credenciales.", |
|||
"Description:Abp.Mailing.Smtp.Domain": "El dominio o equipo que valida las credenciales.", |
|||
"Description:Abp.Mailing.Smtp.EnableSsl": "Si SmtpClient usa Secure Sockets Layer (SSL) para encriptar la conección.", |
|||
"Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Si las credenciales por defecto se envían con los request." |
|||
} |
|||
} |
|||
@ -1,6 +1,6 @@ |
|||
{ |
|||
"culture": "es", |
|||
"texts": { |
|||
"Menu:Administration": "Administración" |
|||
"Menu:Administration": "Administración" |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
$(function () { |
|||
var links = $("a.page-link"); |
|||
|
|||
$.each(links, function (key, value) { |
|||
var oldUrl = links[key].getAttribute("href"); |
|||
var value = Number(oldUrl.match(/currentPage=(\d+)&page/)[1]); |
|||
links[key].setAttribute("href", "/Components/Paginator?currentPage=" + value); |
|||
}) |
|||
}); |
|||
@ -0,0 +1,20 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.test.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj" /> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" /> |
|||
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.Testing; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public abstract class AbpAspNetCoreSignalRTestBase : AbpIntegratedTest<AbpAspNetCoreSignalRTestModule> |
|||
{ |
|||
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
|||
{ |
|||
options.UseAutofac(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using Volo.Abp.Autofac; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpAspNetCoreSignalRModule), |
|||
typeof(AbpTestBaseModule), |
|||
typeof(AbpAutofacModule) |
|||
)] |
|||
public class AbpAspNetCoreSignalRTestModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using Microsoft.Extensions.Options; |
|||
using Shouldly; |
|||
using Volo.Abp.AspNetCore.SignalR.SampleHubs; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR |
|||
{ |
|||
public class AbpSignalROptions_Tests : AbpAspNetCoreSignalRTestBase |
|||
{ |
|||
private readonly AbpSignalROptions _options; |
|||
|
|||
public AbpSignalROptions_Tests() |
|||
{ |
|||
_options = GetRequiredService<IOptions<AbpSignalROptions>>().Value; |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Auto_Add_Maps() |
|||
{ |
|||
_options.Hubs.ShouldContain(h => h.HubType == typeof(RegularHub)); |
|||
_options.Hubs.ShouldContain(h => h.HubType == typeof(RegularAbpHub)); |
|||
_options.Hubs.ShouldNotContain(h => h.HubType == typeof(DisableConventionalRegistrationHub)); |
|||
_options.Hubs.ShouldNotContain(h => h.HubType == typeof(DisableAutoHubMapHub)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using Microsoft.AspNetCore.SignalR; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR.SampleHubs |
|||
{ |
|||
[DisableAutoHubMap] |
|||
public class DisableAutoHubMapHub : Hub |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR.SampleHubs |
|||
{ |
|||
[DisableConventionalRegistration] |
|||
public class DisableConventionalRegistrationHub : Hub |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.AspNetCore.SignalR.SampleHubs |
|||
{ |
|||
public class RegularAbpHub : AbpHub |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Microsoft.AspNetCore.SignalR; |
|||
|
|||
namespace Volo.Abp.AspNetCore.SignalR.SampleHubs |
|||
{ |
|||
public class RegularHub : Hub |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
{ |
|||
"culture": "es", |
|||
"texts": { |
|||
"UserName": "Nombre de usuario", |
|||
"EmailAddress": "Dirección de correo electrónico", |
|||
"UserNameOrEmailAddress": "Nombre de usuario o dirección de correo electrónico", |
|||
"Password": "Contraseña", |
|||
"RememberMe": "Recuérdame", |
|||
"UseAnotherServiceToLogin": "Usar otro servicio para iniciar sesión", |
|||
"UserLockedOutMessage": "La cuenta de usuario ha sido bloqueada debido a los intentos de inicio de sesión no válidos. Por favor, espere unos minutos y vuelve a intentarlo.", |
|||
"InvalidUserNameOrPassword": "El nombre de usuario o la contraseña no son válidos", |
|||
"LoginIsNotAllowed": "No está permitido el inicio de sesión! Usted tendrá que confirmar su correo electrónico o número de teléfono.", |
|||
"SelfRegistrationDisabledMessage": "El autoregistro de usuario está deshabilitado para esta aplicación. Póngase en contacto con el administrador de la aplicación para registrar un nuevo usuario.", |
|||
"LocalLoginDisabledMessage": "No está habilitado el login local para esta aplicación.", |
|||
"Login": "Iniciar sesión", |
|||
"Cancel": "Cancelar", |
|||
"Register": "Registrarse", |
|||
"AreYouANewUser": "¿Usted es un usuario nuevo?", |
|||
"AlreadyRegistered": "¿Está registrado en esta aplicación?", |
|||
"InvalidLoginRequest": "Solicitud de inicio de sesión no válido", |
|||
"ThereAreNoLoginSchemesConfiguredForThisClient": "No hay ningún esquema de inicio de sesión configurado para este cliente.", |
|||
"LogInUsingYourProviderAccount": "Inicia sesión con tu cuenta de {0} ", |
|||
"DisplayName:CurrentPassword": "Contraseña actual", |
|||
"DisplayName:NewPassword": "Nueva contraseña", |
|||
"DisplayName:NewPasswordConfirm": "Confirmar nueva contraseña", |
|||
"PasswordChangedMessage": "Su contraseña ha sido cambiada con éxito.", |
|||
"DisplayName:UserName": "Nombre de usuario", |
|||
"DisplayName:Email": "Correo electrónico", |
|||
"DisplayName:Name": "Nombre", |
|||
"DisplayName:Surname": "Apellido", |
|||
"DisplayName:Password": "Contraseña", |
|||
"DisplayName:EmailAddress": "Dirección de correo electrónico", |
|||
"DisplayName:PhoneNumber": "Número de teléfono", |
|||
"PersonalSettings": "Configuración Personal", |
|||
"PersonalSettingsSaved": "Ajustes personales guardados", |
|||
"PasswordChanged": "Cambiar la contraseña", |
|||
"NewPasswordConfirmFailed": "Por favor, confirme la nueva contraseña.", |
|||
"Manage": "Administrar", |
|||
"ManageYourProfile": "Gestionar su perfil", |
|||
"DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Habilitar el registro de usuario", |
|||
"Description:Abp.Account.IsSelfRegistrationEnabled": "Si está habilitado los usuarios pueden crear una cuenta mediante el registro automático.", |
|||
"DisplayName:Abp.Account.EnableLocalLogin": "Habilitar cuenta local", |
|||
"Description:Abp.Account.EnableLocalLogin": "Indica que el servidor permite iniciar sessión con una cuenta local." |
|||
} |
|||
} |
|||
Loading…
Reference in new issue