Merge branch 'dev' of https://github.com/abpframework/abp into feat/confirmation-dismiss

pull/3910/head
mehmet-erim 5 years ago
commit 49046a3ba8

7
.gitignore vendored

@ -293,3 +293,10 @@ samples/MicroserviceDemo/applications/ConsoleClientDemo/Logs/logs.txt
modules/docs/app/Volo.DocsTestApp/Logs/logs.txt
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Logs/logs.txt
samples/MicroserviceDemo/microservices/TenantManagementService.Host/Logs/logs.txt
/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package-lock.json
/samples/BookStore/src/Acme.BookStore.Web/package-lock.json
/samples/DashboardDemo/src/DashboardDemo.Web/package-lock.json
/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package-lock.json
/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/package-lock.json
/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/package-lock.json
/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/package-lock.json

@ -35,6 +35,9 @@ While there is no Razor Pages & MongoDB combination, you can check both document
* **Entity Framework Migrations**: A solution to demonstrate how to split your application into multiple databases each database contains different modules.
* [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo)
* [EF Core database migrations document](../Entity-Framework-Core-Migrations.md)
* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users.
* [Source code](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo)
* [Signal Integration document](../SignalR-Integration.md)
* **Dashboard Demo**: A simple application to show how to use the widget system for the ASP.NET Core MVC UI.
* [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo)
* [Widget documentation](../UI/AspNetCore/Widgets.md)

@ -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:
![signal-js-file](images/signal-js-file.png)
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.
![signalr-demo-chat](images/signalr-demo-chat.png)

@ -24,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components
public void OnGet(int currentPage, string sort)
{
PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort);
PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

@ -108,6 +108,8 @@ public class MyIdentityUserManager : IdentityUserManager
{
public MyIdentityUserManager(
IdentityUserStore store,
IIdentityRoleRepository roleRepository,
IIdentityUserRepository userRepository,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher,
IEnumerable<IUserValidator<IdentityUser>> userValidators,
@ -116,9 +118,10 @@ public class MyIdentityUserManager : IdentityUserManager
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<IdentityUserManager> logger,
ICancellationTokenProvider cancellationTokenProvider
) : base(
store,
ICancellationTokenProvider cancellationTokenProvider) :
base(store,
roleRepository,
userRepository,
optionsAccessor,
passwordHasher,
userValidators,

@ -30,10 +30,8 @@ ABP是一个模块化框架,它需要一个**启动 (根) 模块**继承自``Abp
````C#
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Volo.Abp;
using Volo.Abp.AspNetCore.Modularity;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Modularity;
@ -42,7 +40,8 @@ namespace BasicAspNetCoreApplication
[DependsOn(typeof(AbpAspNetCoreMvcModule))]
public class AppModule : AbpModule
{
public override void OnApplicationInitialization(ApplicationInitializationContext context)
public override void OnApplicationInitialization(
ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
@ -51,8 +50,14 @@ namespace BasicAspNetCoreApplication
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseMvcWithDefaultRoute();
app.UseStaticFiles();
app.UseRouting();
app.UseConfiguredEndpoints();
}
}
}

@ -157,6 +157,16 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier);
````
* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误!
* 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加
````csharp
options.Scope.Add("email");
````
到你的 openid 配置.
* Help! 我一直得到 ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** 错误!
* 如果你在appsettings设置 **CallbackPath** 为:
@ -170,16 +180,18 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi
你在azure门户的应用程序**重定向URI**必须具有之类 `https://localhost:44320/signin-azuread-oidc`<u></u>, 而不仅是 `/signin-azuread-oidc`.
* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误!
* Help! 我一直得到 ***AADSTS700051: The response_type 'token' is not enabled for the application.*** 错误!
* 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加
* 当你请求**token**(访问令牌)和**id_token**时没有在Azure门户应用程序启用访问令牌时会发生这个错误,只需勾选ID令牌顶部的**访问令牌**复选框即可同时请求令牌.
* Help! 我一直得到 ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** 错误!
* 当你与 **id_token** 一起请求 **code**时,你需要在Azure门户的**证书和机密**菜单下添加**证书和机密**. 然后你需要添加openid配置选项:
````csharp
options.Scope.Add("email");
options.ClientSecret = "Value of your secret on azure portal";
````
到你的 openid 配置.
* 如何**调试/监视**在映射之前获得的声明?
* 你可以在 openid 配置下加一个简单的事件在映射之前进行调试,例如:

@ -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 来实现新增书籍的功能. 最终成果如下图所示:
![bookstore-create-dialog](images/bookstore-create-dialog-2.png)
#### 新建 modal form
`Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面:
![bookstore-add-create-dialog](images/bookstore-add-create-dialog-v2.png)
##### 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** 按钮:
![bookstore-new-book-button](images/bookstore-new-book-button.png)
打开 `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页面:
![bookstore-add-edit-dialog](images/bookstore-add-edit-dialog.png)
#### 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") . 最终效果如下:
![bookstore-books-table-actions](images/bookstore-books-table-actions.png)
打开 `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. -->

@ -24,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components
public void OnGet(int currentPage, string sort)
{
PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort);
PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort);
}
}
}

@ -495,6 +495,10 @@
{
"text": "示例",
"items": [
{
"text": "所有示例",
"path": "Samples/Index.md"
},
{
"text": "微服务示例",
"path": "Samples/Microservice-Demo.md"

@ -287,6 +287,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Validation.Abstrac
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.SignalR", "src\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj", "{B64FCE08-E9D2-4984-BF12-FE199F257416}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.SignalR.Tests", "test\Volo.Abp.AspNetCore.SignalR.Tests\Volo.Abp.AspNetCore.SignalR.Tests.csproj", "{8B758716-DCC9-4223-8421-5588D1597487}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -853,6 +855,10 @@ Global
{B64FCE08-E9D2-4984-BF12-FE199F257416}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.Build.0 = Release|Any CPU
{8B758716-DCC9-4223-8421-5588D1597487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B758716-DCC9-4223-8421-5588D1597487}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B758716-DCC9-4223-8421-5588D1597487}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B758716-DCC9-4223-8421-5588D1597487}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -998,6 +1004,7 @@ Global
{251C7FD3-D313-4BCE-8068-352EC7EEA275} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{B64FCE08-E9D2-4984-BF12-FE199F257416} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{8B758716-DCC9-4223-8421-5588D1597487} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

@ -1,4 +1,6 @@
using System.Text;
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Localization.Resources.AbpUi;
@ -123,6 +125,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination
var tagHelperOutput = await anchorTagHelper.ProcessAndGetOutputAsync(attributeList, context, "a", TagMode.StartTagAndEndTag);
SetHrefAttribute(currentPage, attributeList);
tagHelperOutput.Content.SetHtmlContent(localizer[localizationKey]);
var renderedHtml = tagHelperOutput.Render(_encoder);
@ -172,5 +176,20 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination
" </ nav>\r\n" +
" </div>\r\n";
}
protected virtual void SetHrefAttribute(string currentPage, TagHelperAttributeList attributeList)
{
var hrefAttribute = attributeList.FirstOrDefault(x => x.Name.Equals("href", StringComparison.OrdinalIgnoreCase));
if (hrefAttribute != null)
{
var pageUrl = TagHelper.Model.PageUrl;
var routeValue = $"currentPage={currentPage}{(TagHelper.Model.Sort.IsNullOrWhiteSpace()? "" : "&sort="+TagHelper.Model.Sort)}";
pageUrl += pageUrl.Contains("?") ? "&" + routeValue : "?" + routeValue;
attributeList.Remove(hrefAttribute);
attributeList.Add(new TagHelperAttribute("href", pageUrl, hrefAttribute.ValueStyle));
}
}
}
}

@ -38,7 +38,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination
PageSize = pageSize;
TotalPageCount = (int)Math.Ceiling(Convert.ToDouble((decimal)TotalItemsCount / PageSize));
Sort = sort;
PageUrl = pageUrl;
PageUrl = pageUrl?.EnsureStartsWith('/') ?? "/";
if (currentPage > TotalPageCount)
{

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Modularity;
namespace Volo.Abp.AspNetCore.SignalR
@ -65,7 +66,7 @@ namespace Volo.Abp.AspNetCore.SignalR
services.OnRegistred(context =>
{
if (typeof(Hub).IsAssignableFrom(context.ImplementationType))
if (IsHubClass(context) && !IsDisabledForAutoMap(context))
{
hubTypes.Add(context.ImplementationType);
}
@ -80,6 +81,16 @@ namespace Volo.Abp.AspNetCore.SignalR
});
}
private static bool IsHubClass(IOnServiceRegistredContext context)
{
return typeof(Hub).IsAssignableFrom(context.ImplementationType);
}
private static bool IsDisabledForAutoMap(IOnServiceRegistredContext context)
{
return context.ImplementationType.IsDefined(typeof(DisableAutoHubMapAttribute), true);
}
private void MapHubType(
Type hubType,
IEndpointRouteBuilder endpoints,

@ -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
{
}
}

@ -19,11 +19,17 @@ namespace Volo.Abp.AspNetCore.SignalR
public HubConfig(
[NotNull] Type hubType,
[NotNull] string routePattern)
[NotNull] string routePattern,
[CanBeNull] Action<HttpConnectionDispatcherOptions> configureAction = null)
{
HubType = Check.NotNull(hubType, nameof(hubType));
RoutePattern = Check.NotNullOrWhiteSpace(routePattern, nameof(routePattern));
ConfigureActions = new List<Action<HttpConnectionDispatcherOptions>>();
if (configureAction != null)
{
ConfigureActions.Add(configureAction);
}
}
public static HubConfig Create<THub>()

@ -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);
}
}
}

@ -13,6 +13,11 @@ namespace Volo.Abp.AspNetCore.SignalR
RoutePattern = routePattern;
}
public virtual string GetRoutePatternForType(Type hubType)
{
return RoutePattern;
}
public static string GetRoutePattern<THub>()
where THub : Hub
{
@ -24,7 +29,7 @@ namespace Volo.Abp.AspNetCore.SignalR
var routeAttribute = hubType.GetSingleAttributeOrNull<HubRouteAttribute>();
if (routeAttribute != null)
{
return routeAttribute.RoutePattern;
return routeAttribute.GetRoutePatternForType(hubType);
}
return "/signalr-hubs/" + hubType.Name.RemovePostFix("Hub").ToKebabCase();

@ -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."
}
}

@ -28,5 +28,4 @@
<ProjectReference Include="..\Volo.Abp.Settings\Volo.Abp.Settings.csproj" />
<ProjectReference Include="..\Volo.Abp.VirtualFileSystem\Volo.Abp.VirtualFileSystem.csproj" />
</ItemGroup>
</Project>

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Nito.AsyncEx;
using Volo.Abp.DependencyInjection;
using Volo.Abp.VirtualFileSystem;
@ -9,46 +9,35 @@ namespace Volo.Abp.TextTemplating.VirtualFiles
{
public class LocalizedTemplateContentReaderFactory : ILocalizedTemplateContentReaderFactory, ISingletonDependency
{
private readonly IVirtualFileProvider _virtualFileProvider;
private readonly Dictionary<string, ILocalizedTemplateContentReader> _readerCache;
private readonly ReaderWriterLockSlim _lock;
protected IVirtualFileProvider VirtualFileProvider { get; }
protected ConcurrentDictionary<string, ILocalizedTemplateContentReader> ReaderCache { get; }
protected SemaphoreSlim SyncObj;
public LocalizedTemplateContentReaderFactory(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
_readerCache = new Dictionary<string, ILocalizedTemplateContentReader>();
_lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
VirtualFileProvider = virtualFileProvider;
ReaderCache = new ConcurrentDictionary<string, ILocalizedTemplateContentReader>();
SyncObj = new SemaphoreSlim(1, 1);
}
public async Task<ILocalizedTemplateContentReader> CreateAsync(TemplateDefinition templateDefinition)
public virtual async Task<ILocalizedTemplateContentReader> CreateAsync(TemplateDefinition templateDefinition)
{
_lock.EnterUpgradeableReadLock();
if (ReaderCache.TryGetValue(templateDefinition.Name, out var reader))
{
return reader;
}
try
using (await SyncObj.LockAsync())
{
var reader = _readerCache.GetOrDefault(templateDefinition.Name);
if (reader != null)
if (ReaderCache.TryGetValue(templateDefinition.Name, out reader))
{
return reader;
}
_lock.EnterWriteLock();
try
{
reader = await CreateInternalAsync(templateDefinition);
_readerCache[templateDefinition.Name] = reader;
ReaderCache[templateDefinition.Name] = reader;
return reader;
}
finally
{
_lock.ExitWriteLock();
}
}
finally
{
_lock.ExitUpgradeableReadLock();
}
}
protected virtual async Task<ILocalizedTemplateContentReader> CreateInternalAsync(
@ -60,7 +49,7 @@ namespace Volo.Abp.TextTemplating.VirtualFiles
return NullLocalizedTemplateContentReader.Instance;
}
var fileInfo = _virtualFileProvider.GetFileInfo(virtualPath);
var fileInfo = VirtualFileProvider.GetFileInfo(virtualPath);
if (!fileInfo.Exists)
{
throw new AbpException("Could not find a file/folder at the location: " + virtualPath);
@ -69,7 +58,7 @@ namespace Volo.Abp.TextTemplating.VirtualFiles
if (fileInfo.IsDirectory)
{
var folderReader = new VirtualFolderLocalizedTemplateContentReader();
await folderReader.ReadContentsAsync(_virtualFileProvider, virtualPath);
await folderReader.ReadContentsAsync(VirtualFileProvider, virtualPath);
return folderReader;
}
else //File

@ -1,6 +1,6 @@
{
"culture": "es",
"texts": {
"Menu:Administration": "Administración"
"Menu:Administration": "Administración"
}
}

@ -1,20 +1,20 @@
{
"culture": "es",
"texts": {
"InternalServerErrorMessage": "Ocurrió un error interno en su pedido!",
"ValidationErrorMessage": "Su pedido no es válido!",
"ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validación.",
"DefaultErrorMessage": "Ocurrió un error!",
"DefaultErrorMessageDetail": "El servidor no envió el detalle del error.",
"DefaultErrorMessage401": "Usted no ha iniciado sessión!",
"DefaultErrorMessage401Detail": "Debe iniciar sessión para ejecutar esta operación.",
"DefaultErrorMessage403": "Usted no está autorizado!",
"DefaultErrorMessage403Detail": "Usted no puede hacer esta operación!",
"DefaultErrorMessage404": "No se encontró el recurso!",
"DefaultErrorMessage404Detail": "El recurso solicitado no se encontró en el servidor!",
"InternalServerErrorMessage": "Ocurrió un error interno en su pedido!",
"ValidationErrorMessage": "Su pedido no es válido!",
"ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validación.",
"DefaultErrorMessage": "Ocurrió un error!",
"DefaultErrorMessageDetail": "El servidor no envió el detalle del error.",
"DefaultErrorMessage401": "Usted no ha iniciado sessión!",
"DefaultErrorMessage401Detail": "Debe iniciar sessión para ejecutar esta operación.",
"DefaultErrorMessage403": "Usted no está autorizado!",
"DefaultErrorMessage403Detail": "Usted no puede hacer esta operación!",
"DefaultErrorMessage404": "No se encontró el recurso!",
"DefaultErrorMessage404Detail": "El recurso solicitado no se encontró en el servidor!",
"EntityNotFoundErrorMessage": "No hay una entidad {0} con el id = {1}!",
"Error": "Error",
"AreYouSure": "¿Está seguro?",
"AreYouSure": "¿Está seguro?",
"Cancel": "Cancelar",
"Yes": "Si",
"No": "No",
@ -30,14 +30,14 @@
"Welcome": "Bienvenido",
"Login": "Iniciar session",
"Register": "Registrarse",
"Logout": "Cerrar sessión",
"Logout": "Cerrar sessión",
"Submit": "Enviar",
"Back": "Atrás",
"Back": "Atrás",
"PagerSearch": "Buscar",
"PagerNext": "Siguiente",
"PagerPrevious": "Anterior",
"PagerFirst": "Primero",
"PagerLast": "Último",
"PagerLast": "Último",
"PagerInfo": "Mostrando desde _START_ hasta _END_ total _TOTAL_",
"PagerInfo{0}{1}{2}": "Mostrando desde {0} hasta {1} total {2}",
"PagerInfoEmpty": "Mostrando desde 0 hasta 0 de 0 registros",
@ -45,15 +45,15 @@
"NoDataAvailableInDatatable": "No hay datos",
"PagerShowMenuEntries": "Mostrar _MENU_ registros",
"DatatableActionDropdownDefaultText": "Acciones",
"ChangePassword": "Cambiar contraseña",
"ChangePassword": "Cambiar contraseña",
"PersonalInfo": "My perfil",
"AreYouSureYouWantToCancelEditingWarningMessage": "Tiene cambios sin guardar.",
"UnhandledException": "Excepción no controlada!",
"UnhandledException": "Excepción no controlada!",
"401Message": "No autorizado",
"403Message": "Prohibido",
"404Message": "La página no existe",
"404Message": "La página no existe",
"500Message": "Error interno del servidor",
"GoHomePage": "Ir a la página principal",
"GoHomePage": "Ir a la página principal",
"GoBack": "Atras"
}
}

@ -1,4 +1,4 @@
{
{
"culture": "es",
"texts": {
"'{0}' and '{1}' do not match.": "'{0} \" y \"{1} \" no coinciden.",
@ -11,7 +11,7 @@
"The {0} field is not a valid phone number.": "{0} no es un número de teléfono válido.",
"The field {0} must be between {1} and {2}.": "El campo {0} debe ser entre {1} y {2}.",
"The field {0} must match the regular expression '{1}'.": "El campo {0} no coincide con el formato solicitado.",
"The {0} field is required.": "La {0} campo es obligatorio.",
"The {0} field is required.": "El {0} campo es obligatorio.",
"The field {0} must be a string with a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud máxima de {1}.",
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud mínima de {2} y una longitud máxima de {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "El campo {0} no es una dirección URL válida http, https o ftp.",

@ -47,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components
public void OnGet(int currentPage, string sort)
{
PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort);
PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort);
}
}
}

@ -9,7 +9,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components
public void OnGet(int currentPage, string sort)
{
PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort);
PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort);
}
}
}

@ -7,11 +7,6 @@
@{
PageLayout.Content.Title = "Paginator";
}
@section scripts {
<abp-script-bundle name="@typeof(IndexModel).FullName">
<abp-script src="/Pages/Components/Paginator/index.js" />
</abp-script-bundle>
}
<h2>Paginator</h2>

@ -9,7 +9,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Paginator
public void OnGet(int currentPage = 1, string sort = null)
{
PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort);
PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort);
}
}
}

@ -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
{
}
}

@ -1,4 +1,4 @@
{
{
"culture": "es",
"texts": {
"USA": "Estados unidos de América",

@ -1,4 +1,4 @@
{
{
"culture": "es",
"texts": {
"ThisFieldIsRequired": "El campo no puede estar vacío",

@ -1,4 +1,4 @@
{
{
"culture": "es",
"texts": {
"Hello <b>{0}</b>.": "Hola <b>{0}</b>.",

@ -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."
}
}

@ -82,6 +82,7 @@
"DisplayName:Abp.Identity.Lockout.LockoutDuration": "鎖定時間(秒)",
"DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts": "最大失敗存取嘗試次數",
"DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail": "要求驗證的電子信箱",
"DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "啟用手機號碼驗證",
"DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "要求驗證的手機號碼",
"DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled": "啟用使用者名稱更新",
"DisplayName:Abp.Identity.User.IsEmailUpdateEnabled": "啟用電子信箱更新",
@ -95,9 +96,9 @@
"Description:Abp.Identity.Lockout.LockoutDuration": "當鎖定發生時使用者被鎖定的時間(秒).",
"Description:Abp.Identity.Lockout.MaxFailedAccessAttempts": "如果啟用鎖定,當使用者被鎖定前失敗的存取嘗試次數.",
"Description:Abp.Identity.SignIn.RequireConfirmedEmail": "登入時是否需要驗證電子信箱.",
"Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "使用者手機號碼是否需要驗證.",
"Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "登入時是否需要驗證手機號碼.",
"Description:Abp.Identity.User.IsUserNameUpdateEnabled": "是否允許使用者更新使用者名稱.",
"Description:Abp.Identity.User.IsEmailUpdateEnabled": "是否允許使用者更新電子信箱."
}
}

@ -19,7 +19,4 @@ export const environment = {
url: 'https://localhost:44305',
},
},
localization: {
defaultResourceName: 'MyProjectName',
},
};

@ -19,7 +19,4 @@ export const environment = {
url: 'https://localhost:44305',
},
},
localization: {
defaultResourceName: 'MyProjectName',
},
};

@ -2,7 +2,7 @@ import { ABP } from '../models';
export class SetLanguage {
static readonly type = '[Session] Set Language';
constructor(public payload: string) {}
constructor(public payload: string, public dispatchAppConfiguration?: boolean) {}
}
export class SetTenant {
static readonly type = '[Session] Set Tenant';

@ -66,5 +66,6 @@ export namespace ApplicationConfiguration {
id: string;
tenantId: string;
userName: string;
email: string;
}
}

@ -16,7 +16,7 @@ export namespace Config {
hmr?: boolean;
oAuthConfig: AuthConfig;
apis: Apis;
localization: { defaultResourceName: string };
localization?: { defaultResourceName?: string };
}
export interface Application {

@ -1,11 +1,12 @@
import { Injectable, NgZone, Optional, SkipSelf } from '@angular/core';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { Store, Actions, ofActionSuccessful } from '@ngxs/store';
import { Actions, ofActionSuccessful, Store } from '@ngxs/store';
import { noop, Observable } from 'rxjs';
import { SetLanguage } from '../actions/session.actions';
import { ApplicationConfiguration } from '../models/application-configuration';
import { Config } from '../models/config';
import { ConfigState } from '../states/config.state';
import { registerLocale } from '../utils/initial-utils';
import { Config } from '../models/config';
import { SetLanguage } from '../actions/session.actions';
type ShouldReuseRoute = (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) => boolean;
@ -75,4 +76,31 @@ export class LocalizationService {
instant(key: string | Config.LocalizationWithDefault, ...interpolateParams: string[]): string {
return this.store.selectSnapshot(ConfigState.getLocalization(key, ...interpolateParams));
}
isLocalized(key, sourceName) {
if (sourceName === '_') {
// A convention to suppress the localization
return true;
}
const localization = this.store.selectSnapshot(
ConfigState.getOne('localization'),
) as ApplicationConfiguration.Localization;
sourceName = sourceName || localization.defaultResourceName;
if (!sourceName) {
return false;
}
const source = localization.values[sourceName];
if (!source) {
return false;
}
const value = source[key];
if (value === undefined) {
return false;
}
return true;
}
}

@ -155,33 +155,45 @@ export class ConfigState {
}
const keys = key.split('::') as string[];
const selector = createSelector([ConfigState], (state: Config.State) => {
if (!state.localization) return defaultValue || key;
const defaultResourceName = snq(() => state.environment.localization.defaultResourceName);
if (keys[0] === '') {
if (!defaultResourceName) {
throw new Error(
`Please check your environment. May you forget set defaultResourceName?
Here is the example:
{ production: false,
localization: {
defaultResourceName: 'MyProjectName'
}
}`,
);
const selector = createSelector([ConfigState], (state: Config.State): string => {
const warn = (message: string) => {
if (!state.environment.production) console.warn(message);
};
if (keys.length < 2) {
warn('The localization source separator (::) not found.');
return defaultValue || (key as string);
}
if (!state.localization) return defaultValue || keys[1];
keys[0] = defaultResourceName;
const sourceName =
keys[0] ||
snq(() => state.environment.localization.defaultResourceName) ||
state.localization.defaultResourceName;
const sourceKey = keys[1];
if (sourceName === '_') {
return defaultValue || sourceKey;
}
let localization = (keys as any).reduce((acc, val) => {
if (acc) {
return acc[val];
if (!sourceName) {
warn(
'Localization source name is not specified and the defaultResourceName was not defined!',
);
return defaultValue || sourceKey;
}
return undefined;
}, state.localization.values);
const source = state.localization.values[sourceName];
if (!source) {
warn('Could not find localization source: ' + sourceName);
return defaultValue || sourceKey;
}
let localization = source[sourceKey];
if (typeof localization === 'undefined') {
return defaultValue || sourceKey;
}
interpolateParams = interpolateParams.filter(params => params != null);
if (localization && interpolateParams && interpolateParams.length) {
@ -191,7 +203,7 @@ export class ConfigState {
}
if (typeof localization !== 'string') localization = '';
return localization || defaultValue || key;
return localization || defaultValue || (key as string);
});
return selector;
@ -219,9 +231,13 @@ export class ConfigState {
defaultLang = defaultLang.split(';')[0];
}
document.documentElement.setAttribute(
'lang',
configuration.localization.currentCulture.cultureName,
);
return this.store.selectSnapshot(SessionState.getLanguage)
? of(null)
: dispatch(new SetLanguage(defaultLang));
: dispatch(new SetLanguage(defaultLang, false));
}),
catchError(err => {
dispatch(new RestOccurError(new HttpErrorResponse({ status: 0, error: err })));

@ -69,12 +69,15 @@ export class SessionState {
}
@Action(SetLanguage)
setLanguage({ patchState, dispatch }: StateContext<Session.State>, { payload }: SetLanguage) {
setLanguage(
{ patchState, dispatch }: StateContext<Session.State>,
{ payload, dispatchAppConfiguration = true }: SetLanguage,
) {
patchState({
language: payload,
});
return dispatch(new GetAppConfiguration());
if (dispatchAppConfiguration) return dispatch(new GetAppConfiguration());
}
@Action(SetTenant)

@ -122,6 +122,7 @@ const CONFIG_STATE_DATA = {
id: null,
tenantId: null,
userName: null,
email: null,
},
features: {
values: {},

@ -146,6 +146,7 @@ export const CONFIG_STATE_DATA = {
id: null,
tenantId: null,
userName: null,
email: null,
},
features: {
values: {},
@ -271,7 +272,7 @@ describe('ConfigState', () => {
);
expect(ConfigState.getLocalization('AbpIdentity::NoIdentity')(CONFIG_STATE_DATA)).toBe(
'AbpIdentity::NoIdentity',
'NoIdentity',
);
expect(
@ -286,18 +287,15 @@ describe('ConfigState', () => {
)(CONFIG_STATE_DATA),
).toBe('first and second do not match.');
try {
expect(
ConfigState.getLocalization('::Test')({
...CONFIG_STATE_DATA,
environment: {
...CONFIG_STATE_DATA.environment,
localization: {} as any,
},
});
expect(false).toBeTruthy(); // fail
} catch (error) {
expect((error as Error).message).toContain('Please check your environment');
}
}),
).toBe('Test');
});
});

@ -44,13 +44,7 @@ describe('LocalizationService', () => {
describe('#instant', () => {
it('should be return a localization', () => {
store.selectSnapshot.andCallFake(
(selector: (state: any, ...states: any[]) => Observable<string>) => {
return selector({
ConfigState: { getLocalization: (keys, ...interpolateParams) => keys },
});
},
);
store.selectSnapshot.andReturn('AbpTest');
expect(service.instant('AbpTest')).toBe('AbpTest');
});

@ -28,7 +28,15 @@ const publish = async () => {
await execa(
'yarn',
['lerna', 'version', program.nextVersion, '--yes', '--no-commit-hooks', '--skip-git'],
[
'lerna',
'version',
program.nextVersion,
'--yes',
'--no-commit-hooks',
'--skip-git',
'--force-publish',
],
{ stdout: 'inherit', cwd: '../' },
);

@ -2,7 +2,7 @@ export const environment = {
production: true,
application: {
name: 'MyProjectName',
logoUrl: ''
logoUrl: '',
},
oAuthConfig: {
issuer: 'https://localhost:44305',
@ -11,14 +11,11 @@ export const environment = {
scope: 'MyProjectName',
showDebugInformation: true,
oidc: false,
requireHttps: true
requireHttps: true,
},
apis: {
default: {
url: 'https://localhost:44305'
}
url: 'https://localhost:44305',
},
},
localization: {
defaultResourceName: 'MyProjectName'
}
};

@ -2,7 +2,7 @@ export const environment = {
production: false,
application: {
name: 'MyProjectName',
logoUrl: ''
logoUrl: '',
},
oAuthConfig: {
issuer: 'https://localhost:44305',
@ -11,14 +11,11 @@ export const environment = {
scope: 'MyProjectName',
showDebugInformation: true,
oidc: false,
requireHttps: true
requireHttps: true,
},
apis: {
default: {
url: 'https://localhost:44305'
}
url: 'https://localhost:44305',
},
},
localization: {
defaultResourceName: 'MyProjectName'
}
};

@ -18,7 +18,4 @@ export const environment = {
url: 'https://localhost:44300',
},
},
localization: {
defaultResourceName: 'MyProjectName',
},
};

@ -18,7 +18,4 @@ export const environment = {
url: 'https://localhost:44300',
},
},
localization: {
defaultResourceName: 'MyProjectName',
},
};

Loading…
Cancel
Save