Browse Source

Merge branch 'dev' into maliming/UserSecurityLog

pull/4675/head
maliming 6 years ago
parent
commit
9c0719503b
  1. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json
  2. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json
  3. 12
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  4. 2
      docs/en/Modules/Identity.md
  5. 2
      docs/en/Modules/Index.md
  6. 47
      docs/en/Modules/Organization-Units.md
  7. 226
      docs/en/Tutorials/Part-1.md
  8. 575
      docs/en/Tutorials/Part-2.md
  9. BIN
      docs/en/Tutorials/images/bookstore-actions-buttons.png
  10. BIN
      docs/en/Tutorials/images/bookstore-angular-file-tree.png
  11. BIN
      docs/en/Tutorials/images/bookstore-book-list.png
  12. BIN
      docs/en/Tutorials/images/bookstore-confirmation-popup.png
  13. BIN
      docs/en/Tutorials/images/bookstore-empty-new-book-modal.png
  14. 114
      docs/en/UI/Angular/List-Service.md
  15. 370
      docs/en/UI/Angular/Migration-Guide-v3.md
  16. 216
      docs/en/UI/Angular/Modifying-the-Menu.md
  17. BIN
      docs/en/UI/Angular/images/table-of-packages-to-update.png
  18. 290
      docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md
  19. 2
      docs/en/Unit-Of-Work.md
  20. 180
      docs/en/Virtual-File-System.md
  21. 4
      docs/en/docs-nav.json
  22. 3
      docs/zh-Hans/CLI.md
  23. 4
      docs/zh-Hans/Entity-Framework-Core-MySQL.md
  24. 60
      docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md
  25. 67
      docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md
  26. 67
      docs/zh-Hans/Entity-Framework-Core-Oracle.md
  27. 4
      docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md
  28. 4
      docs/zh-Hans/Entity-Framework-Core-SQLite.md
  29. 224
      docs/zh-Hans/Tutorials/Part-1.md
  30. 596
      docs/zh-Hans/Tutorials/Part-2.md
  31. BIN
      docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png
  32. BIN
      docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png
  33. BIN
      docs/zh-Hans/Tutorials/images/bookstore-book-list.png
  34. BIN
      docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png
  35. BIN
      docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png
  36. 89
      docs/zh-Hans/UI/Angular/List-Service.md
  37. 363
      docs/zh-Hans/UI/Angular/Migration-Guide-v3.md
  38. 220
      docs/zh-Hans/UI/Angular/Modifying-the-Menu.md
  39. BIN
      docs/zh-Hans/UI/Angular/images/table-of-packages-to-update.png
  40. 278
      docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md
  41. 2
      docs/zh-Hans/Unit-Of-Work.md
  42. 172
      docs/zh-Hans/Virtual-File-System.md
  43. 4
      docs/zh-Hans/docs-nav.json
  44. 14
      framework/Volo.Abp.sln
  45. 27
      framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Properties/launchSettings.json
  46. 27
      framework/src/Volo.Abp.AspNetCore.MultiTenancy/Properties/launchSettings.json
  47. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.Client/Properties/launchSettings.json
  48. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs
  49. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Properties/launchSettings.json
  50. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Properties/launchSettings.json
  51. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Properties/launchSettings.json
  52. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Properties/launchSettings.json
  53. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Uppy/UppyScriptContributor.cs
  54. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Uppy/UppyStyleContributor.cs
  55. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Properties/launchSettings.json
  56. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj
  57. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj
  58. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/AbpPageSearchBoxViewComponent.cs
  59. 10
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/Default.cshtml
  60. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/_ViewImports.cshtml
  61. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Properties/launchSettings.json
  62. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj
  63. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Properties/launchSettings.json
  64. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI/Properties/launchSettings.json
  65. 27
      framework/src/Volo.Abp.AspNetCore.Mvc/Properties/launchSettings.json
  66. 5
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs
  67. 1
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs
  68. 3
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs
  69. 34
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/AbpValidationAttributeAdapterProvider.cs
  70. 46
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicMaxLengthAttributeAdapter.cs
  71. 54
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicStringLengthAttributeAdapter.cs
  72. 27
      framework/src/Volo.Abp.AspNetCore.Serilog/Properties/launchSettings.json
  73. 27
      framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json
  74. 27
      framework/src/Volo.Abp.AspNetCore.TestBase/Properties/launchSettings.json
  75. 27
      framework/src/Volo.Abp.AspNetCore/Properties/launchSettings.json
  76. 6
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpBackgroundWorkersQuartzModule.cs
  77. 7
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/IQuartzBackgroundWorkerAdapter.cs
  78. 88
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerAdapter.cs
  79. 18
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs
  80. 1
      framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs
  81. 52
      framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobNamingNormalizer.cs
  82. 9
      framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs
  83. 41
      framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobNamingNormalizer.cs
  84. 62
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs
  85. 10
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs
  86. 24
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs
  87. 9
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobNamingNormalizer.cs
  88. 3
      framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xml
  89. 30
      framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xsd
  90. 25
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj
  91. 30
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs
  92. 294
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs
  93. 47
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs
  94. 393
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs
  95. 30
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/ICacheSupportsMultipleItems.cs
  96. 63
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs
  97. 38
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs
  98. 47
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs
  99. 6
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AngularEnvironmentFilePortChangeForSeparatedIdentityServersStep.cs
  100. 91
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs

3
abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json

@ -8,6 +8,7 @@
"FrameworkDocumentation": "Framework documentation", "FrameworkDocumentation": "Framework documentation",
"OfficialBlog": "Official blog", "OfficialBlog": "Official blog",
"CommercialHomePage": "Commercial home page", "CommercialHomePage": "Commercial home page",
"CommercialSupportWebSite": "Commercial support web site" "CommercialSupportWebSite": "Commercial support web site",
"CommunityWebSite": "ABP community web site"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json

@ -8,6 +8,7 @@
"FrameworkDocumentation": "Framework Dokümantasyon", "FrameworkDocumentation": "Framework Dokümantasyon",
"OfficialBlog": "Resmi blog", "OfficialBlog": "Resmi blog",
"CommercialHomePage": "Kurumsal ana sayfa", "CommercialHomePage": "Kurumsal ana sayfa",
"CommercialSupportWebSite": "Kurumsal destek web sitesi" "CommercialSupportWebSite": "Kurumsal destek web sitesi",
"CommunityWebSite": "ABP topluluk web sitesi"
} }
} }

12
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -102,7 +102,16 @@
"DoYouWantToCreateNewUser": "Do you want to create new user?", "DoYouWantToCreateNewUser": "Do you want to create new user?",
"MasterModules": "Master Modules", "MasterModules": "Master Modules",
"OrganizationName": "Organization name", "OrganizationName": "Organization name",
"CreationDate": "Creation date",
"LicenseStart": "License start date",
"LicenseEndDate": "License end date",
"OrganizationNamePlaceholder": "Organization name...", "OrganizationNamePlaceholder": "Organization name...",
"TotalQuestionCountPlaceholder": "Total question count...",
"RemainingQuestionCountPlaceholder": "Remaining question count...",
"LicenseTypePlaceholder": "License type...",
"CreationDatePlaceholder": "Creation date...",
"LicenseStartDatePlaceholder": "License start date...",
"LicenseEndDatePlaceholder": "License end date...",
"UsernameOrEmail": "Username or email", "UsernameOrEmail": "Username or email",
"UsernameOrEmailPlaceholder": "Username or email...", "UsernameOrEmailPlaceholder": "Username or email...",
"Member": "Member", "Member": "Member",
@ -156,6 +165,7 @@
"RemainingQuestionCount": "Remaining question count", "RemainingQuestionCount": "Remaining question count",
"TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount !", "TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount !",
"QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero !", "QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero !",
"UnlimitedQuestionCount": "Unlimited question count" "UnlimitedQuestionCount": "Unlimited question count",
"Notes": "Notes"
} }
} }

2
docs/en/Modules/Identity.md

@ -1,4 +1,6 @@
# Identity Management Module # Identity Management Module
Identity module is used to manage [organization units](Organization-Units.md), roles, users and their permissions, based on the Microsoft Identity library.
See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon... See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon...

2
docs/en/Modules/Index.md

@ -17,7 +17,7 @@ There are some **free and open source** application modules developed and mainta
* **Blogging**: Used to create fancy blogs. ABP's [own blog](https://blog.abp.io/) already using this module. * **Blogging**: Used to create fancy blogs. ABP's [own blog](https://blog.abp.io/) already using this module.
* [**Docs**](Docs.md): Used to create technical documentation pages. ABP's [own documentation](https://docs.abp.io) already using this module. * [**Docs**](Docs.md): Used to create technical documentation pages. ABP's [own documentation](https://docs.abp.io) already using this module.
* **Feature Management**: Used to persist and manage the [features](../Features.md). * **Feature Management**: Used to persist and manage the [features](../Features.md).
* **Identity**: Manages roles, users and their permissions, based on the Microsoft Identity library. * **[Identity](Identity.md)**: Manages organization units, roles, users and their permissions, based on the Microsoft Identity library.
* **IdentityServer**: Integrates to IdentityServer4. * **IdentityServer**: Integrates to IdentityServer4.
* **Permission Management**: Used to persist permissions. * **Permission Management**: Used to persist permissions.
* **[Setting Management](Setting-Management.md)**: Used to persist and manage the [settings](../Settings.md). * **[Setting Management](Setting-Management.md)**: Used to persist and manage the [settings](../Settings.md).

47
docs/en/Modules/Organization-Units.md

@ -0,0 +1,47 @@
# Organization Unit Management
Organization units (OU) is a part of **Identity Module** and can be used to **hierarchically group users and entities**.
### OrganizationUnit Entity
An OU is represented by the **OrganizationUnit** entity. The fundamental properties of this entity are:
- **TenantId**: Tenant's Id of this OU. Can be null for host OUs.
- **ParentId**: Parent OU's Id. Can be null if this is a root OU.
- **Code**: A hierarchical string code that is unique for a tenant.
- **DisplayName**: Shown name of the OU.
The OrganizationUnit entity's primary key (Id) is a **Guid** type and it derives from the [**FullAuditedAggregateRoot**](../Entities.md) class.
#### Organization Tree
Since an OU can have a parent, all OUs of a tenant are in a **tree** structure. There are some rules for this tree;
- There can be more than one root (where the `ParentId` is `null`).
- There is a limit for the first-level children count of an OU (because of the fixed OU Code unit length explained below).
#### OU Code
OU code is automatically generated and maintained by the OrganizationUnit Manager. It's a string that looks something like this:
"**00001.00042.00005**"
This code can be used to easily query the database for all the children of an OU (recursively). There are some rules for this code:
- It must be **unique** for a [tenant](../Multi-Tenancy.md).
- All the children of the same OU have codes that **start with the parent OU's code**.
- It's **fixed length** and based on the level of the OU in the tree, as shown in the sample.
- While the OU code is unique, it can be **changeable** if you move an OU.
- You must reference an OU by Id, not Code.
### OrganizationUnit Manager
The **OrganizationUnitManager** class can be [injected](../Dependency-Injection.md) and used to manage OUs. Common use cases are:
- Create, Update or Delete an OU
- Move an OU in the OU tree.
- Getting information about the OU tree and its items.
#### Multi-Tenancy
The `OrganizationUnitManager` is designed to work for a **single tenant** at a time. It works for the **current tenant** by default.

226
docs/en/Tutorials/Part-1.md

@ -754,8 +754,8 @@ It's end of this part. The final UI of this work is shown as below:
It's time to create something visible and usable! There are some tools that we will use when developing ABP Angular frontend application: It's time to create something visible and usable! There are some tools that we will use when developing ABP Angular frontend application:
- [Angular CLI](https://angular.io/cli) will be used to create modules, components and services. - [Angular CLI](https://angular.io/cli) will be used to create modules, components and services.
- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library.
- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library. - [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library.
- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) will be used as the datatable library.
- [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor). - [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor).
#### Install NPM packages #### Install NPM packages
@ -778,26 +778,56 @@ yarn ng generate module book --routing true
#### Routing #### Routing
Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and add a route as shown below Open the `app-routing.module.ts` file in `src\app` folder and add a route as shown below:
```js ```js
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <== const routes: Routes = [
// ...
// added a new route to the routes array
{
path: 'books',
loadChildren: () => import('./book/book.module').then(m => m.BookModule)
}
]
```
//...added books path with the below to the routes array * We added a lazy-loaded route. See the [Lazy-Loading Feature Modules](https://angular.io/guide/lazy-loading-ngmodules#lazy-loading-feature-modules).
{
path: 'books', Open the `route.provider.ts` file in `src\app` folder and replace the content as below:
component: ApplicationLayoutComponent,
loadChildren: () => import('./book/book.module').then(m => m.BookModule), ```js
data: { import { RoutesService, eLayoutType } from '@abp/ng.core';
routes: { import { APP_INITIALIZER } from '@angular/core';
name: '::Menu:Books',
iconClass: 'fas fa-book' export const APP_ROUTE_PROVIDER = [
} as ABP.Route { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
}, ];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
//...
// added below element
{
path: '/books',
name: '::Menu:Books',
iconClass: 'fas fa-book',
order: 101,
layout: eLayoutType.application,
},
]);
};
} }
``` ```
* The `ApplicationLayoutComponent` configuration sets the application layout to the new page. We added the `data` object. The `name` is the menu item name and the `iconClass` is the icon of the menu item. * We added a new route element to show a navigation element labeled "Books" on the menu.
* `path` is the URL of the route.
* `name` is the menu item name. A Localization key can be passed.
* `iconClass` is the icon of the menu item.
* `order` is the order of the menu item. We define 101 to show the route after the "Administration" item.
* `layout` is the layout of the BooksModule's routes. `eLayoutType.application`, `eLayoutType.account` or `eLayoutType.empty` can be defined.
For more information, see the [RoutesService document](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice).
#### Book list component #### Book list component
@ -865,35 +895,6 @@ Open the browser and navigate to http://localhost:4200/books. We'll see **book-l
![Initial book list page](./images/bookstore-initial-book-list-page.png) ![Initial book list page](./images/bookstore-initial-book-list-page.png)
#### Create BookState
Run the following command in the terminal to create a new state, named `BooksState`:
```bash
npx @ngxs/cli --name book --directory src/app/book
```
* This command creates `book.state.ts` and `book.actions.ts` files in the `src/app/book/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli).
Import the `BookState` to the `app.module.ts` in the `src/app` folder and then add the `BookState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method.
```js
// ...
import { BookState } from './books/state/book.state'; //<== imported BookState ==>
@NgModule({
imports: [
// other imports
NgxsModule.forRoot([BookState]), //<== added BookState ==>
//other imports
],
// ...
})
export class AppModule {}
```
#### Generate proxies #### Generate proxies
ABP CLI provides `generate-proxy` command that generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running generate-proxy command, your host must be up and running. See the [CLI documentation](../CLI.md) ABP CLI provides `generate-proxy` command that generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running generate-proxy command, your host must be up and running. See the [CLI documentation](../CLI.md)
@ -910,109 +911,41 @@ The generated files looks like below:
![Generated files](./images/generated-proxies.png) ![Generated files](./images/generated-proxies.png)
#### GetBooks Action
Actions can either be thought of as a command which should trigger something to happen, or as the resulting event of something that has already happened. [See NGXS Actions documentation](https://www.ngxs.io/concepts/actions).
Open the `book.actions.ts` file in `app/book/state` folder and replace the content below:
```js
export class GetBooks {
static readonly type = '[Book] Get';
}
```
#### Implement BookState
Open the `book.state.ts` file in `app/book/state` folder and replace the content below:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks } from './book.actions';
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
book: booksResponse,
});
})
);
}
}
```
* We added the book property to BookStateModel model.
* We added the `GetBooks` action that retrieves the book data via `BookService` that generated via ABP CLI and patches the state.
* `NGXS` requires to return the observable without subscribing it in the get function.
#### BookListComponent #### BookListComponent
Open the `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: Open the `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions'; import { BookService } from '../services';
import { BookState } from '../state/book.state';
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [ListService],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
books$: Observable<BookDto[]>;
booksType = BookType; booksType = BookType;
loading = false; constructor(public readonly list: ListService, private bookService: BookService) {}
constructor(private store: Store) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
} }
``` ```
* We added the `get` function that updates store to get the books. * We imported and injected the generated `BookService`.
* See the [Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the `NGXS` documentation for more information on these `NGXS` features. * We implemented the [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service) that is a utility service to provide easy pagination, sorting, and search implementation.
Open the `book-list.component.html` file in `app\book\book-list` folder and replace the content as below: Open the `book-list.component.html` file in `app\book\book-list` folder and replace the content as below:
@ -1022,38 +955,31 @@ Open the `book-list.component.html` file in `app\book\book-list` folder and repl
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<h5 class="card-title"> <h5 class="card-title">
{%{{{ "::Menu:Books" | abpLocalization }}}%} {%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5> </h5>
</div> </div>
<div class="text-right col col-md-6"></div> <div class="text-right col col-md-6"></div>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<abp-table <ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
[value]="books$ | async" <ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
[abpLoading]="loading" <ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
[headerTemplate]="tableHeader" <ng-template let-row="row" ngx-datatable-cell-template>
[bodyTemplate]="tableBody" {%{{{ booksType[row.type] }}}%}
[rows]="10" </ng-template>
[scrollable]="true" </ngx-datatable-column>
> <ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
</abp-table> <ng-template let-row="row" ngx-datatable-cell-template>
<ng-template #tableHeader> {%{{{ row.publishDate | date }}}%}
<tr> </ng-template>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th> </ngx-datatable-column>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th> <ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th> <ng-template let-row="row" ngx-datatable-cell-template>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th> {%{{{ row.price | currency }}}%}
</tr> </ng-template>
</ng-template> </ngx-datatable-column>
<ng-template #tableBody let-data> </ngx-datatable>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ booksType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
</div> </div>
</div> </div>
``` ```
@ -1074,4 +1000,4 @@ In this tutorial we have applied the rules of official [Angular Style Guide](htt
### Next Part ### Next Part
See the [part 2](part-2.md) for creating, updating and deleting books. See the [part 2](./Part-2.md) for creating, updating and deleting books.

575
docs/en/Tutorials/Part-2.md

@ -454,78 +454,49 @@ Run the application and try to delete a book.
In this section, you will learn how to create a new modal dialog form to create a new book. In this section, you will learn how to create a new modal dialog form to create a new book.
#### State definitions #### Add a modal to BookListComponent
Open `book.action.ts` in `app\book\state` folder and replace the content as below: Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js ```js
import { CreateUpdateBookDto } from '../models'; //<== added this line ==> import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
export class GetBooks { import { BookDto, BookType } from '../models';
static readonly type = '[Book] Get'; import { BookService } from '../services';
}
// added CreateUpdateBook class
export class CreateUpdateBook {
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto) { }
}
```
* We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action. @Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [ListService],
})
export class BookListComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
Open `book.state.ts` file in `app\book\state` folder and replace the content as below: booksType = BookType;
```js isModalOpen = false; // <== added this line ==>
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel { constructor(public readonly list: ListService, private bookService: BookService) {}
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({ ngOnInit() {
name: 'BookState', const bookStreamCreator = (query) => this.bookService.getListByInput(query);
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {} this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
@Action(GetBooks) });
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((bookResponse) => {
ctx.patchState({
book: bookResponse,
});
})
);
} }
// added CreateUpdateBook action listener // added createBook method
@Action(CreateUpdateBook) createBook() {
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) { this.isModalOpen = true;
return this.bookService.createByInput(action.payload);
} }
} }
``` ```
* We imported `CreateUpdateBook` action and defined the `save` method that will listen to a `CreateUpdateBook` action to create a book. * We defined a variable called `isModalOpen` and `createBook` method.
* We added the `createBook` method.
When the `SaveBook` action dispatched, the save method is being executed. It calls `createByInput` method of the `BookService`.
#### Add a modal to BookListComponent
Open `book-list.component.html` file in `books\book-list` folder and replace the content as below: Open `book-list.component.html` file in `books\book-list` folder and replace the content as below:
@ -534,19 +505,12 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
<div class="card-header"> <div class="card-header">
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<h5 class="card-title"> <h5 class="card-title">{%{{{ '::Menu:Books' | abpLocalization }}}%}</h5>
{%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5>
</div> </div>
<!--Added new book button --> <!--Added new book button -->
<div class="text-right col col-md-6"> <div class="text-right col col-md-6">
<div class="text-lg-right pt-2"> <div class="text-lg-right pt-2">
<button <button id="create" class="btn btn-primary" type="button" (click)="createBook()">
id="create"
class="btn btn-primary"
type="button"
(click)="createBook()"
>
<i class="fa fa-plus mr-1"></i> <i class="fa fa-plus mr-1"></i>
<span>{%{{{ "::NewBook" | abpLocalization }}}%}</span> <span>{%{{{ "::NewBook" | abpLocalization }}}%}</span>
</button> </button>
@ -555,47 +519,40 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<abp-table <ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
[value]="books$ | async" <ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
[abpLoading]="loading" <ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
[headerTemplate]="tableHeader" <ng-template let-row="row" ngx-datatable-cell-template>
[bodyTemplate]="tableBody" {%{{{ booksType[row.type] }}}%}
[rows]="10" </ng-template>
[scrollable]="true" </ngx-datatable-column>
> <ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
</abp-table> <ng-template let-row="row" ngx-datatable-cell-template>
<ng-template #tableHeader> {%{{{ row.publishDate | date }}}%}
<tr> </ng-template>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th> </ngx-datatable-column>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th> <ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th> <ng-template let-row="row" ngx-datatable-cell-template>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th> {%{{{ row.price | currency }}}%}
</tr> </ng-template>
</ng-template> </ngx-datatable-column>
<ng-template #tableBody let-data> </ngx-datatable>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ booksType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
</div> </div>
</div> </div>
<!--added modal--> <!--added modal-->
<abp-modal [(visible)]="isModalOpen"> <abp-modal [(visible)]="isModalOpen">
<ng-template #abpHeader> <ng-template #abpHeader>
<h3>{%{{{ '::NewBook' | abpLocalization }}}%}</h3> <h3>{%{{{ '::NewBook' | abpLocalization }}}%}</h3>
</ng-template> </ng-template>
<ng-template #abpBody> </ng-template> <ng-template #abpBody> </ng-template>
<ng-template #abpFooter> <ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose> <button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpAccount::Close' | abpLocalization }}}%} {%{{{ 'AbpAccount::Close' | abpLocalization }}}%}
</button> </button>
</ng-template> </ng-template>
</abp-modal> </abp-modal>
``` ```
@ -603,55 +560,6 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
* `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. * `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits.
* We added `New book` button to the `AbpContentToolbar`. * We added `New book` button to the `AbpContentToolbar`.
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
loading = false;
isModalOpen = false; // <== added this line ==>
constructor(private store: Store) {}
ngOnInit() {
this.get();
}
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
}
// added createBook method
createBook() {
this.isModalOpen = true;
}
}
```
* We added `isModalOpen = false` and `createBook` method.
You can open your browser and click **New book** button to see the new modal. You can open your browser and click **New book** button to see the new modal.
![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) ![Empty modal for new book](./images/bookstore-empty-new-book-modal.png)
@ -663,48 +571,43 @@ You can open your browser and click **New book** button to see the new modal.
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions'; import { BookService } from '../services';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==> import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [ListService],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
books$: Observable<BookDto[]>;
booksType = BookType; booksType = BookType;
loading = false;
isModalOpen = false; isModalOpen = false;
form: FormGroup; // <== added this line ==> form: FormGroup; // <== added this line ==>
constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==> constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder // <== injected FormBuilder ==>
) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
this.buildForm(); //<== added this line ==> this.buildForm(); // <== added this line ==>
this.isModalOpen = true; this.isModalOpen = true;
} }
@ -799,13 +702,10 @@ export class BookModule {}
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions'; import { BookService } from '../services';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==> import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==>
@ -813,37 +713,34 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==> providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added a provide ==>
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
books$: Observable<BookDto[]>;
booksType = BookType; booksType = BookType;
//added bookTypeArr array // <== added bookTypeArr array ==>
bookTypeArr = Object.keys(BookType).filter( bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false;
isModalOpen = false; isModalOpen = false;
form: FormGroup; form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) {} constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -884,13 +781,10 @@ Now, you can open your browser to see the changes:
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==> import { BookService } from '../services';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -898,11 +792,10 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
books$: Observable<BookDto[]>;
booksType = BookType; booksType = BookType;
@ -910,24 +803,22 @@ export class BookListComponent implements OnInit {
(bookType) => typeof this.booksType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false;
isModalOpen = false; isModalOpen = false;
form: FormGroup; form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) {} constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -950,19 +841,18 @@ export class BookListComponent implements OnInit {
return; return;
} }
this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { this.bookService.createByInput(this.form.value).subscribe(() => {
this.isModalOpen = false; this.isModalOpen = false;
this.form.reset(); this.form.reset();
this.get(); this.list.get();
}); });
} }
} }
``` ```
* We imported `CreateUpdateBook`.
* We added `save` method * We added `save` method
Open `book-list.component.html` in `app\book\book-list` folder and add the following `abp-button` to save the new book. Open `book-list.component.html` in `app\book\book-list` folder, find the `<ng-template #abpFooter>` element and replace this element with the following to create a new book.
```html ```html
<ng-template #abpFooter> <ng-template #abpFooter>
@ -994,63 +884,24 @@ The final modal UI looks like below:
### Updating a book ### Updating a book
#### CreateUpdateBook action Open `book-list.component.ts` in `app\book\book-list` folder and add a variable named `selectedBook`.
Open the `book.actions.ts` in `app\book\state` folder and replace the content as below:
```js
import { CreateUpdateBookDto } from '../models';
export class GetBooks {
static readonly type = '[Book] Get';
}
export class CreateUpdateBook {
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==>
}
```
* We added `id` parameter to the `CreateUpdateBook` action's constructor.
Open the `book.state.ts` in `app\book\state` folder and replace the `save` method as below:
```js
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
return this.bookService.createByInput(action.payload);
}
}
```
#### BookListComponent
Open `book-list.component.ts` in `app\book\book-list` folder and inject `BookService` dependency by adding it to the constructor and add a variable named `selectedBook`.
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions'; import { BookService } from '../services';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { BookService } from '../services'; // <== imported BookService ==>
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
books$: Observable<BookDto[]>;
booksType = BookType; booksType = BookType;
@ -1058,26 +909,24 @@ export class BookListComponent implements OnInit {
(bookType) => typeof this.booksType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false;
isModalOpen = false; isModalOpen = false;
form: FormGroup; form: FormGroup;
selectedBook = {} as BookDto; // <== declared selectedBook ==> selectedBook = {} as BookDto; // <== declared selectedBook ==>
constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==> constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
// <== this method is replaced ==> // <== this method is replaced ==>
@ -1109,30 +958,31 @@ export class BookListComponent implements OnInit {
}); });
} }
// <== this method is replaced ==>
save() { save() {
if (this.form.invalid) { if (this.form.invalid) {
return; return;
} }
//<== added this.selectedBook.id ==> // <== added request ==>
this.store const request = this.selectedBook.id
.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) ? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id)
.subscribe(() => { : this.bookService.createByInput(this.form.value);
this.isModalOpen = false;
this.form.reset(); request.subscribe(() => {
this.get(); this.isModalOpen = false;
}); this.form.reset();
this.list.get();
});
} }
} }
``` ```
* We imported `BookService`.
* We declared a variable named `selectedBook` as `BookDto`. * We declared a variable named `selectedBook` as `BookDto`.
* We injected `BookService` to the constructor. `BookService` is being used to retrieve the book data which is being edited.
* We added `editBook` method. This method fetches the book with the given `Id` and sets it to `selectedBook` object. * We added `editBook` method. This method fetches the book with the given `Id` and sets it to `selectedBook` object.
* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data. * We replaced the `buildForm` method so that it creates the form with the `selectedBook` data.
* We replaced the `createBook` method so it sets `selectedBook` to an empty object. * We replaced the `createBook` method so it sets `selectedBook` to an empty object.
* We added `selectedBook.id` to the constructor of the new `CreateUpdateBook`. * We replaced the `save` method.
#### Add "Actions" dropdown to the table #### Add "Actions" dropdown to the table
@ -1140,27 +990,14 @@ Open the `book-list.component.html` in `app\book\book-list` folder and replace
```html ```html
<div class="card-body"> <div class="card-body">
<abp-table <ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
[value]="books$ | async" <!-- added actions column -->
[abpLoading]="loading" <ngx-datatable-column
[headerTemplate]="tableHeader" [name]="'::Actions' | abpLocalization"
[bodyTemplate]="tableBody" [maxWidth]="150"
[rows]="10" [sortable]="false"
[scrollable]="true" >
> <ng-template let-row="row" ngx-datatable-cell-template>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{%{{{ "::Actions" | abpLocalization }}}%}</th>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th>
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>
<div ngbDropdown container="body" class="d-inline-block"> <div ngbDropdown container="body" class="d-inline-block">
<button <button
class="btn btn-primary btn-sm dropdown-toggle" class="btn btn-primary btn-sm dropdown-toggle"
@ -1168,25 +1005,37 @@ Open the `book-list.component.html` in `app\book\book-list` folder and replace
aria-haspopup="true" aria-haspopup="true"
ngbDropdownToggle ngbDropdownToggle
> >
<i class="fa fa-cog mr-1"></i>{%{{{ "::Actions" | abpLocalization }}}%} <i class="fa fa-cog mr-1"></i>{%{{{ '::Actions' | abpLocalization }}}%}
</button> </button>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<button ngbDropdownItem (click)="editBook(data.id)"> <button ngbDropdownItem (click)="editBook(row.id)">
{%{{{ "::Edit" | abpLocalization }}}%} {%{{{ '::Edit' | abpLocalization }}}%}
</button> </button>
</div> </div>
</div> </div>
</td> </ng-template>
<td>{%{{{ data.name }}}%}</td> </ngx-datatable-column>
<td>{%{{{ booksType[data.type] }}}%}</td> <ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<td>{%{{{ data.publishDate | date }}}%}</td> <ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<td>{%{{{ data.price }}}%}</td> <ng-template let-row="row" ngx-datatable-cell-template>
</tr> {%{{{ booksType[row.type] }}}%}
</ng-template> </ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.publishDate | date }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.price | currency }}}%}
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</div> </div>
``` ```
- We added a `th` for the "Actions" column. - We added a `ngx-datatable-column` for the "Actions" column.
- We added `button` with `ngbDropdownToggle` to open actions when clicked the button. - We added `button` with `ngbDropdownToggle` to open actions when clicked the button.
- We have used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. - We have used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions.
@ -1198,7 +1047,7 @@ Open `book-list.component.html` in `app\book\book-list` folder and find the `<ng
```html ```html
<ng-template #abpHeader> <ng-template #abpHeader>
<h3>{%{{{ (selectedBook.id ? 'AbpIdentity::Edit' : '::NewBook' ) | abpLocalization }}}%}</h3> <h3>{%{{{ (selectedBook.id ? '::Edit' : '::NewBook' ) | abpLocalization }}}%}</h3>
</ng-template> </ng-template>
``` ```
@ -1206,81 +1055,9 @@ Open `book-list.component.html` in `app\book\book-list` folder and find the `<ng
### Deleting a book ### Deleting a book
#### DeleteBook action
Open `book.actions.ts` in `app\book\state` folder and add an action named `DeleteBook`.
```js
export class DeleteBook {
static readonly type = '[Book] Delete';
constructor(public id: string) {}
}
```
Open the `book.state.ts` in `app\book\state` folder and replace the content as below:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
book: booksResponse,
});
})
);
}
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
return this.bookService.createByInput(action.payload);
}
}
// <== added DeleteBook action listener ==>
@Action(DeleteBook)
delete(ctx: StateContext<BookStateModel>, action: DeleteBook) {
return this.bookService.deleteById(action.id);
}
}
```
- We imported `DeleteBook` .
- We added `DeleteBook` action listener to the end of the file.
#### Delete confirmation popup #### Delete confirmation popup
Open `book-list.component.ts` in`app\book\book-list` folder and inject the `ConfirmationService`. Open `book-list.component.ts` in `app\book\book-list` folder and inject the `ConfirmationService`.
Replace the constructor as below: Replace the constructor as below:
@ -1289,11 +1066,11 @@ import { ConfirmationService } from '@abp/ng.theme.shared';
//... //...
constructor( constructor(
private store: Store, public readonly list: ListService,
private fb: FormBuilder, private bookService: BookService,
private bookService: BookService, private fb: FormBuilder,
private confirmation: ConfirmationService // <== added this line ==> private confirmation: ConfirmationService // <== added this line ==>
) { } ) {}
``` ```
* We imported `ConfirmationService`. * We imported `ConfirmationService`.
@ -1301,28 +1078,24 @@ constructor(
See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service) See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)
In the `book-list.component.ts` add a delete method : In the `book-list.component.ts` add a delete method:
```js ```js
import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==> import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation namespace ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==>
//... //...
delete(id: string) { delete(id: string) {
this.confirmation this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => {
.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') if (status === Confirmation.Status.confirm) {
.subscribe(status => { this.bookService.deleteById(id).subscribe(() => this.list.get());
if (status === Confirmation.Status.confirm) { }
this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get()); });
}
});
} }
``` ```
The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below: The `delete` method shows a confirmation popup and subscribes for the user response. The `deleteById` method of `BookService` called only if user clicks to the `Yes` button. The confirmation popup looks like below:
![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) ![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png)
@ -1335,7 +1108,7 @@ Open `book-list.component.html` in `app\book\book-list` folder and modify the `n
```html ```html
<div ngbDropdownMenu> <div ngbDropdownMenu>
<!-- added Delete button --> <!-- added Delete button -->
<button ngbDropdownItem (click)="delete(data.id)"> <button ngbDropdownItem (click)="delete(row.id)">
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%} {%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
</button> </button>
</div> </div>

BIN
docs/en/Tutorials/images/bookstore-actions-buttons.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 25 KiB

BIN
docs/en/Tutorials/images/bookstore-angular-file-tree.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/en/Tutorials/images/bookstore-book-list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/en/Tutorials/images/bookstore-confirmation-popup.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/en/Tutorials/images/bookstore-empty-new-book-modal.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 56 KiB

114
docs/en/UI/Angular/List-Service.md

@ -53,59 +53,41 @@ class BookComponent {
} }
``` ```
> 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. > Noticed `list` is `public` and `readonly`? That is because we will use `ListService` 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 members to expose the `ListService` instance instead.
Place `ListService` properties into the template like this: Bind `ListService` to ngx-datatable like this:
```html ```html
<abp-table <ngx-datatable
[value]="book.items" [rows]="items"
[(page)]="list.page" [count]="count"
[rows]="list.maxResultCount" [list]="list"
[totalRecords]="book.totalCount" default
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[abpLoading]="list.isLoading$ | async"
> >
</abp-table> <!-- column templates here -->
</ngx-datatable>
<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 ## 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: 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 ```js
book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query)); book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query));
``` ```
```html ```html
<!-- simplified representation of the template --> <!-- simplified representation of the template -->
<abp-table <ngx-datatable
[value]="(book$ | async)?.items || []" [rows]="(book$ | async)?.items || []"
[totalRecords]="(book$ | async)?.totalCount" [count]="(book$ | async)?.totalCount || 0"
[list]="list"
default
> >
</abp-table> <!-- column templates here -->
</ngx-datatable>
<!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE --> <!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE -->
``` ```
@ -113,7 +95,7 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide
...or... ...or...
```ts ```js
@Select(BookState.getBooks) @Select(BookState.getBooks)
books$: Observable<BookDto[]>; books$: Observable<BookDto[]>;
@ -128,32 +110,41 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide
```html ```html
<!-- simplified representation of the template --> <!-- simplified representation of the template -->
<abp-table <ngx-datatable
[value]="books$ | async" [rows]="(books$ | async) || []"
[totalRecords]="bookCount$ | async" [count]="(bookCount$ | async) || 0"
[list]="list"
default
> >
</abp-table> <!-- column templates here -->
</ngx-datatable>
``` ```
> We do not recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page.
## How to Refresh Table on Create/Update/Delete ## 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. `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 ```js
this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get); this.bookService.createByInput(form.value)
.subscribe(() => {
this.list.get();
// Other subscription logic here
});
``` ```
...or... ...or...
```ts ```js
this.bookService.createByInput(form.value) this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get);
.subscribe(() => {
this.list.get();
// Other subscription logic here
})
``` ```
> We donot recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page.
## How to Implement Server-Side Search in a Table ## 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. `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.
@ -163,3 +154,26 @@ this.bookService.createByInput(form.value)
<input type="text" name="search" [(ngModel)]="list.filter"> <input type="text" name="search" [(ngModel)]="list.filter">
``` ```
## Breaking Change with ABP v3.0
We had to modify the `ListService` to make it work with `ngx-datatable`. Previously, the minimum value for `page` property was `1` and you could use it like this:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[(page)]="list.page"
></abp-table>
```
As of v3.0, with ngx-datatable, the `page` property has to be set as `0` for inital page. Therefore, if you used `ListService` on your tables before and are going to keep `abp-table`, you need to make the following change:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[page]="list.page + 1"
(pageChange)="list.page = $event - 1"
></abp-table>
```
**Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable.

370
docs/en/UI/Angular/Migration-Guide-v3.md

@ -0,0 +1,370 @@
# v2.9 to v3.0 Angular UI Migration Guide
## What Changed in v3.0?
### Angular 10
The new ABP Angular UI is based on Angular 10 and TypeScript 3.9, and we have dropped support for Angular 8. Nevertheless, ABP modules will keep working with Angular 9. Therefore, if your project is Angular 9, you do not need to update to Angular 10. The update is usually very easy though.
#### What to Do When Migrating?
Open a terminal at your root folder and run the following command:
```sh
yarn ng update @angular/cli @angular/core --force
```
This will make the following modifications:
- Update your package.json and install new packages
- Revise tsconfig.json files to create a "Solution Style" configuration
- Rename `browserlist` as `.browserlistrc`
On the other hand, it would be better if you check which packages to update first with `yarn ng update` command alone. Angular will give you a list of packages to update.
![Table of packages to update](./images/table-of-packages-to-update.png)
When Angular reports the packages above, your command would look like this:
```sh
yarn ng update @angular/cli @angular/core ng-zorro-antd --force
```
> If Angular complains about uncommited changes in your repo, you can either commit/stash them or add `--allow-dirty` parameter to the command.
### Config Modules
In ABP v2.x, every lazy loaded module had a config module available via a separate package and module configuration was as follows:
```js
import { AccountConfigModule } from '@abp/ng.account.config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot({ redirectUrl: '/' }),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
...and in app-routing.module.ts...
```js
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import(
'./lazy-libs/account-wrapper.module'
).then(m => m.AccountWrapperModule),
},
];
```
Although working, this had a few disadvantages:
- Every module came in two independent packages, but in reality, those packages were interdependent.
- Configuring lazy loaded modules required a wrapper module.
- ABP Commercial had extensibility system and configuring extensible modules at the root module was increasing the bundle size.
In ABP v3.0, we have introduced a secondary entry points for each config module as well as a new way to configure lazy loaded modules without the wrappers. Now, the module configuration looks like this:
```js
import { AccountConfigModule } from '@abp/ng.account/config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
...and in app-routing.module.ts...
```js
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
];
```
This change helped us reduce bundle size and build times substantially. We believe you will notice the difference in your apps.
#### A Better Example
AppModule:
```js
import { AccountConfigModule } from '@abp/ng.account/config';
import { CoreModule } from '@abp/ng.core';
import { IdentityConfigModule } from '@abp/ng.identity/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config';
import { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgxsModule } from '@ngxs/store';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
CoreModule.forRoot({
environment,
sendNullsAsQueryParam: false,
skipGetAppConfiguration: false,
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot(),
IdentityConfigModule.forRoot(),
TenantManagementConfigModule.forRoot(),
SettingManagementConfigModule.forRoot(),
ThemeBasicModule.forRoot(),
NgxsModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
AppRoutingModule:
```js
import { DynamicLayoutComponent } from '@abp/ng.core';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./home/home.module')
.then(m => m.HomeModule),
},
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
{
path: 'identity',
loadChildren: () => import('@abp/ng.identity')
.then(m => m.IdentityModule.forLazy()),
},
{
path: 'tenant-management',
loadChildren: () => import('@abp/ng.tenant-management')
.then(m => m.TenantManagementModule.forLazy()),
},
{
path: 'setting-management',
loadChildren: () => import('@abp/ng.setting-management')
.then(m => m.SettingManagementModule.forLazy()),
},
],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
```
> You may have noticed that we used `DynamicLayoutComponent` at top level route component. We made this change in order to avoid unnecessary renders and flickering. It is not mandatory, but we recommend doing the same in your app routing.
#### What to Do When Migrating?
- Remove config packages from your project using `yarn remove`.
- Import config modules from secondary entry points (e.g. `@abp/ng.identity/config`).
- Call static `forRoot` method of all new config modules, even if a configuration is not passed.
- Call static `forRoot` method of `ThemeBasicModule` (or `ThemeLeptonModule` if commercial) and remove `SharedModule` from imports (unless you have added anything that is necessary for your root module in it).
- Import lazy ABP modules directly in app routing module (e.g. `() => import('@abp/ng.identity').then(...)`).
- Call static `forLazy` method of all lazy modules inside `then`, even if a configuration is not passed.
- [OPTIONAL] Add an empty parent route with `DynamicLayoutComponent` for better performance and UX.
### RoutesService
In ABP v2.x, adding a route to the menu was done by one of two ways:
- [Via `routes` Property in `AppRoutingModule`](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule)
- [Via ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate)
As of v3.0, we have changed how routes are added and modified. We are no longer storing routes in `ConfigState` (breaking change). Instead, there is a new service called `RoutesService` which is used for adding, patching, or removing menu items. Please check [the documentation](./Modifying-the-Menu.md) for details.
#### What to Do When Migrating?
- Check if you have ever used `ConfigState` or `ConfigStateService` to add any routes. If so, replace them with `add` method of `RoutesService`.
- Check if you have ever patched a route. If so, convert them to `patch` method of `RoutesService`.
- Double-check that you are using absolute paths and providing a `parentName` instead of `children` property for sub-menu items in `add` or `patch` method calls.
### NavItemsService
In ABP v2.x, adding a navigation element was done [via LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu)
As of v3.0, we have changed how navigation items are added and modified and previous method of doing so is no longer available (breaking change). Please check [the documentation](./Modifying-the-Menu.md) for details.
#### What to Do When Migrating?
- Replace all `dispatchAddNavigationElement` calls with `addItems` method of `NavItemsService`.
### ngx-datatable
Until v3, we had been using a custom component, `abp-table`, as the default table. However, data grids are complicated components and implementing a fully-featured one requires considerable effort, which we are planning to put in to other features and issues.
As of ABP v3, we have switched to a battle-tested, well-executed data grid: [ngx-datatable](https://github.com/swimlane/ngx-datatable). All ABP modules will come with ngx-datatable already implemented in them. `ThemeSharedModule` already exports `NgxDatatableModule`. So, if you install the package by running `yarn add @swimlane/ngx-datatable` in your terminal, it will be available for use in all modules of your app.
For proper styling, you need to add the following in the styles section of your angular.json file:
```json
{
"input": "node_modules/@swimlane/ngx-datatable/index.css",
"inject": true,
"bundleName": "ngx-datatable-index"
},
{
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css",
"inject": true,
"bundleName": "ngx-datatable-icons"
},
{
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css",
"inject": true,
"bundleName": "ngx-datatable-material"
}
```
Since `abp-table` is not dropped yet, modules previously built by ABP v2.x will not suddenly lose all their tables. Yet, they will look and feel different from built-in ABP v3 modules. Therefore, you will probably want to convert the tables in those modules to ngx-datatable. In order to decrease the amount of work required to convert an abp-table into ngx-datatable, we have modified the [ListService](./List-Service.md) to work well with ngx-datatable and [introduced](https://volosoft.com/blog/attribute-directives-to-avoid-repetition-in-angular-templates) two new directives: `NgxDatatableListDirective` and `NgxDatatableDefaultDirective`.
The usage of those directives is rather simple:
```js
@Component({
providers: [ListService],
})
export class SomeComponent {
data$ = this.list.hookToQuery(
query => this.dataService.get(query)
);
constructor(
public readonly list: ListService,
public readonly dataService: SomeDataService,
) {}
}
```
...and in component template...
```html
<ngx-datatable
[rows]="(data$ | async)?.items || []"
[count]="(data$ | async)?.totalCount || 0"
[list]="list"
default
>
<!-- column templates here -->
</ngx-datatable>
```
Once you bind the injected `ListService` instance through `NgxDatatableListDirective`, you no longer need to worry about pagination or sorting. Similarly, `NgxDatatableDefaultDirective` gets rid of several property bindings to make ngx-datatable fit our styles.
#### A Better Example
```html
<ngx-datatable
[rows]="items"
[count]="count"
[list]="list"
default
>
<!-- the grid actions column -->
<ngx-datatable-column
name=""
[maxWidth]="150"
[width]="150"
[sortable]="false"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="rowIndex"
>
<abp-grid-actions
[index]="i"
[record]="row"
text="AbpUi::Actions"
></abp-grid-actions>
</ng-template>
</ngx-datatable-column>
<!-- a basic column -->
<ngx-datatable-column
prop="someProp"
[name]="'::SomeProp' | abpLocalization"
[width]="200"
></ngx-datatable-column>
<!-- a column with a custom template -->
<ngx-datatable-column
prop="someOtherProp"
[name]="'::SomeOtherProp' | abpLocalization"
[width]="250"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="index"
>
<div abpEllipsis>{%{{{ row.someOtherProp }}}%}</div>
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
```
#### What to Do When Migrating?
- Install `@swimlane/ngx-datatable` package.
- Add ngx-datatable styles in the angular.json file.
- If you can, update your modules according to the example above.
- If you have to do that later and are planning to keep abp-table for a while, make sure you update your pagination according to the [breaking change described here](./List-Service.md).
**Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable.
### Deprecated Interfaces
Some interfaces have long been marked as deprecated and now they are removed.
#### What to Do When Migrating?
- Please check if you are still using [anything listed in this issue](https://github.com/abpframework/abp/issues/4281)
## What's Next?
* [Service Proxies](./Service-Proxies.md)

216
docs/en/UI/Angular/Modifying-the-Menu.md

@ -25,9 +25,99 @@ export const environment = {
## How to Add a Navigation Element ## How to Add a Navigation Element
### Via `RoutesService`
You can add routes to the menu by calling the `add` method of `RoutesService`. It is a singleton service, i.e. provided in root, so you can inject and use it immediately.
```js
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { Component } from '@angular/core';
@Component(/* component metadata */)
export class AppComponent {
constructor(routes: RoutesService) {
routes.add([
{
path: '/your-path',
name: 'Your navigation',
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
layout: eLayoutType.application,
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
order: 1,
requiredPolicy: 'permission key here',
},
]);
}
}
```
An alternative and probably cleaner way is to use a route provider. First create a provider:
```js
// route.provider.ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
path: '/your-path',
name: 'Your navigation',
requiredPolicy: 'permission key here',
order: 101,
iconClass: 'fas fa-question-circle',
layout: eLayoutType.application,
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
requiredPolicy: 'permission key here',
order: 1,
},
]);
};
}
```
...and then in app.module.ts...
```js
import { NgModule } from '@angular/core';
import { APP_ROUTE_PROVIDER } from './route.provider';
@NgModule({
providers: [APP_ROUTE_PROVIDER],
// imports, declarations, and bootstrap
})
export class AppModule {}
```
Here is what every property works as:
- `path` is the absolute path of the navigation element.
- `name` is the label of the navigation element. A localization key or a localization object can be passed.
- `parentName` is a reference to the `name` of the parent route in the menu and is used for creating multi-level menu items.
- `requiredPolicy` is the permission key to access the page. See the [Permission Management document](./Permission-Management.md)
- `order` is the order of the navigation element. "Administration" has an order of `100`, so keep that in mind when ordering top level menu items.
- `iconClass` is the class of the `i` tag, which is placed to the left of the navigation label.
- `layout` defines in which layout the route will be loaded. (default: `eLayoutType.empty`)
- `invisible` makes the item invisible in the menu. (default: `false`)
### Via `routes` Property in `AppRoutingModule` ### Via `routes` Property in `AppRoutingModule`
You can define your routes by adding `routes` as a child property to `data` property of a route configuration in the `app-routing.module`. The `@abp/ng.core` package organizes your routes and stores them in the `ConfigState`. `ApplicationLayoutComponent` gets routes from store and displays them on the menu. You can define your routes by adding `routes` as a child property to `data` property of a route configuration in the `app-routing.module`. The `@abp/ng.core` package organizes your routes and stores them in the `RoutesService`.
You can add the `routes` property like below: You can add the `routes` property like below:
@ -37,7 +127,7 @@ You can add the `routes` property like below:
data: { data: {
routes: { routes: {
name: 'Your navigation', name: 'Your navigation',
order: 3, order: 101,
iconClass: 'fas fa-question-circle', iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here', requiredPolicy: 'permission key here',
children: [ children: [
@ -48,99 +138,77 @@ You can add the `routes` property like below:
requiredPolicy: 'permission key here', requiredPolicy: 'permission key here',
}, },
], ],
} as ABP.Route, // can be imported from @abp/ng.core },
} },
} }
``` ```
- `name` is the label of the navigation element. A localization key or a localization object can be passed. Alternatively, you can do this:
- `order` is the order of the navigation element.
- `iconClass` is the class of the `i` tag, which is placed to the left of the navigation label.
- `requiredPolicy` is the permission key to access the page. See the [Permission Management document](./Permission-Management.md)
- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property.
After adding the `routes` property as described above, the navigation menu looks like this:
![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## Via ConfigState
The `dispatchAddRoute` method of `ConfigStateService` adds a new navigation element to the menu.
```js ```js
// this.config is instance of ConfigStateService {
path: 'your-path',
const newRoute: ABP.Route = { data: {
name: 'My New Page', routes: [
iconClass: 'fa fa-dashboard', {
path: 'page', path: '/your-path',
invisible: false, name: 'Your navigation',
order: 2, order: 101,
requiredPolicy: 'MyProjectName.MyNewPage', iconClass: 'fas fa-question-circle',
} as Omit<ABP.Route, 'children'>; requiredPolicy: 'permission key here',
},
this.config.dispatchAddRoute(newRoute); {
// returns a state stream which emits after dispatch action is complete path: '/your-path/child',
``` name: 'Your child navigation',
parentName: 'Your navigation',
The `newRoute` will be placed as at root level, i.e. without any parent routes, and its url will be stored as `'/path'`. order: 1,
requiredPolicy: 'permission key here',
If you want **to add a child route, you can do this:** },
] as ABP.Route[], // can be imported from @abp/ng.core
```js },
// this.config is instance of ConfigStateService }
// eIdentityRouteNames enum can be imported from @abp/ng.identity
const newRoute: ABP.Route = {
parentName: eIdentityRouteNames.IdentityManagement,
name: 'My New Page',
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,
order: 3,
requiredPolicy: 'MyProjectName.MyNewPage'
} as Omit<ABP.Route, 'children'>;
this.config.dispatchAddRoute(newRoute);
// returns a state stream which emits after dispatch action is complete
``` ```
The `newRoute` will then be placed as a child of the parent route named `eIdentityRouteNames.IdentityManagement` and its url will be set as `'/identity/page'`. The advantage of the second method is that you are not bound to the parent/child structure and use any paths you like.
The new route will be added like below: After adding the `routes` property as described above, the navigation menu looks like this:
![navigation-menu-via-config-state](./images/navigation-menu-via-config-state.png) ![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## How to Patch a Navigation Element ## How to Patch or Remove a Navigation Element
The `dispatchPatchRouteByName` method finds a route by its name and replaces its configuration in the store with the new configuration passed as the second parameter. The `patch` method of `RoutesService` finds a route by its name and replaces its configuration with the new configuration passed as the second parameter. Similarly, `remove` method finds a route and removes it along with its children.
```js ```js
// this.config is instance of ConfigStateService // this.routes is instance of RoutesService
// eIdentityRouteNames enum can be imported from @abp/ng.identity // eThemeSharedRouteNames enum can be imported from @abp/ng.theme.shared
const dashboardRouteConfig: ABP.Route = {
path: '/dashboard',
name: '::Menu:Dashboard',
parentName: '::Menu:Home',
order: 1,
layout: eLayoutType.application,
};
const newRouteConfig: Partial<ABP.Route> = { const newHomeRouteConfig: Partial<ABP.Route> = {
iconClass: 'fas fa-home', iconClass: 'fas fa-home',
parentName: eIdentityRouteNames.Administration, parentName: eThemeSharedRouteNames.Administration,
order: 0, order: 0,
children: [
{
name: 'Dashboard',
path: 'dashboard',
},
],
}; };
this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig); this.routes.add([dashboardRouteConfig]);
// returns a state stream which emits after dispatch action is complete this.routes.patch('::Menu:Home', newHomeRouteConfig);
this.routes.remove(['Your navigation']);
``` ```
* Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`. - Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`.
* Added an icon. - Added an icon to _Home_.
* Specified the order. - Specified the order and made _Home_ the first item in list.
* Added a child route named _Dashboard_. - Added a route named _Dashboard_ as a child of _Home_.
- Removed _Your navigation_ along with its child route.
After the patch above, navigation elements looks like below: After the operations above, the new menu looks like below:
![navigation-menu-after-patching](./images/navigation-menu-after-patching.png) ![navigation-menu-after-patching](./images/navigation-menu-after-patching.png)

BIN
docs/en/UI/Angular/images/table-of-packages-to-update.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

290
docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md

@ -11,92 +11,95 @@ Basic usage:
````xml ````xml
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/> <abp-dynamic-form abp-model="@Model.MyDetailedModel"/>
```` ````
Model: Model:
````csharp ````csharp
public class DynamicFormsModel : PageModel public class DynamicFormsModel : PageModel
{ {
[BindProperty] [BindProperty]
public DetailedModel MyDetailedModel { get; set; } public DetailedModel MyDetailedModel { get; set; }
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem> public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem>
{ {
new SelectListItem { Value = "CA", Text = "Canada"}, new SelectListItem { Value = "CA", Text = "Canada"},
new SelectListItem { Value = "US", Text = "USA"}, new SelectListItem { Value = "US", Text = "USA"},
new SelectListItem { Value = "UK", Text = "United Kingdom"}, new SelectListItem { Value = "UK", Text = "United Kingdom"},
new SelectListItem { Value = "RU", Text = "Russia"} new SelectListItem { Value = "RU", Text = "Russia"}
}; };
public void OnGet() public void OnGet()
{ {
MyDetailedModel = new DetailedModel MyDetailedModel = new DetailedModel
{ {
Name = "", Name = "",
Description = "Lorem ipsum dolor sit amet.", Description = "Lorem ipsum dolor sit amet.",
IsActive = true, IsActive = true,
Age = 65, Age = 65,
Day = DateTime.Now, Day = DateTime.Now,
MyCarType = CarType.Coupe, MyCarType = CarType.Coupe,
YourCarType = CarType.Sedan, YourCarType = CarType.Sedan,
Country = "RU", Country = "RU",
NeighborCountries = new List<string>() { "UK", "CA" } NeighborCountries = new List<string>() { "UK", "CA" }
}; };
} }
public class DetailedModel public class DetailedModel
{ {
[Required] [Required]
[Placeholder("Enter your name...")] [Placeholder("Enter your name...")]
[Display(Name = "Name")] [Display(Name = "Name")]
public string Name { get; set; } public string Name { get; set; }
[TextArea(Rows = 4)] [TextArea(Rows = 4)]
[Display(Name = "Description")] [Display(Name = "Description")]
[InputInfoText("Describe Yourself")] [InputInfoText("Describe Yourself")]
public string Description { get; set; } public string Description { get; set; }
[Required] [Required]
[DataType(DataType.Password)] [DataType(DataType.Password)]
[Display(Name = "Password")] [Display(Name = "Password")]
public string Password { get; set; } public string Password { get; set; }
[Display(Name = "Is Active")] [Display(Name = "Is Active")]
public bool IsActive { get; set; } public bool IsActive { get; set; }
[Required] [Required]
[Display(Name = "Age")] [Display(Name = "Age")]
public int Age { get; set; } public int Age { get; set; }
[Required] [Required]
[Display(Name = "My Car Type")] [Display(Name = "My Car Type")]
public CarType MyCarType { get; set; } public CarType MyCarType { get; set; }
[Required] [Required]
[AbpRadioButton(Inline = true)] [AbpRadioButton(Inline = true)]
[Display(Name = "Your Car Type")] [Display(Name = "Your Car Type")]
public CarType YourCarType { get; set; } public CarType YourCarType { get; set; }
[DataType(DataType.Date)] [DataType(DataType.Date)]
[Display(Name = "Day")] [Display(Name = "Day")]
public DateTime Day { get; set; } public DateTime Day { get; set; }
[SelectItems(nameof(CountryList))] [SelectItems(nameof(CountryList))]
[Display(Name = "Country")] [Display(Name = "Country")]
public string Country { get; set; } public string Country { get; set; }
[SelectItems(nameof(CountryList))] [SelectItems(nameof(CountryList))]
[Display(Name = "Neighbor Countries")] [Display(Name = "Neighbor Countries")]
public List<string> NeighborCountries { get; set; } public List<string> NeighborCountries { get; set; }
} }
public enum CarType public enum CarType
{ {
Sedan, Sedan,
Hatchback, Hatchback,
StationWagon, StationWagon,
Coupe Coupe
}
} }
}
```` ````
## Demo ## Demo
See the [dynamic forms demo page](https://bootstrap-taghelpers.abp.io/Components/DynamicForms) to see it in action. See the [dynamic forms demo page](https://bootstrap-taghelpers.abp.io/Components/DynamicForms) to see it in action.
@ -125,7 +128,7 @@ Default value is `True`.
## Form Content Placement ## Form Content Placement
By default, `abp-dynamicform` clears the inner html and places the inputs into itself. If you want to add additional content to dynamic form or place the inputs to some specific area, you can use ` <abp-form-content />` tag. This tag will be replaced by form content and rest of the inner html of `abp-dynamic-form` tag will be unchanged. By default, `abp-dynamic-form` clears the inner html and places the inputs into itself. If you want to add additional content to dynamic form or place the inputs to some specific area, you can use ` <abp-form-content />` tag. This tag will be replaced by form content and rest of the inner html of `abp-dynamic-form` tag will be unchanged.
Usage: Usage:
@ -152,23 +155,23 @@ Default `DisplayOrder` attribute number is 10000 for every property.
See example below: See example below:
````csharp ````csharp
public class OrderExampleModel public class OrderExampleModel
{ {
[DisplayOrder(10004)] [DisplayOrder(10004)]
public string Name{ get; set; } public string Name{ get; set; }
[DisplayOrder(10005)] [DisplayOrder(10005)]
public string Surname{ get; set; } public string Surname{ get; set; }
//Default 10000 //Default 10000
public string EmailAddress { get; set; } public string EmailAddress { get; set; }
[DisplayOrder(10003)] [DisplayOrder(10003)]
public string PhoneNumber { get; set; } public string PhoneNumber { get; set; }
[DisplayOrder(9999)] [DisplayOrder(9999)]
public string City { get; set; } public string City { get; set; }
} }
```` ````
In this example, input fields will be displayed with this order: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`. In this example, input fields will be displayed with this order: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`.
@ -198,55 +201,57 @@ If you have read the [Form elements document](Form-elements.md), you noticed tha
````xml ````xml
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/> <abp-dynamic-form abp-model="@Model.MyDetailedModel"/>
```` ````
Model: Model:
````csharp ````csharp
public class DynamicFormsModel : PageModel public class DynamicFormsModel : PageModel
{ {
[BindProperty] [BindProperty]
public DetailedModel MyDetailedModel { get; set; } public DetailedModel MyDetailedModel { get; set; }
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem> public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem>
{ {
new SelectListItem { Value = "CA", Text = "Canada"}, new SelectListItem { Value = "CA", Text = "Canada"},
new SelectListItem { Value = "US", Text = "USA"}, new SelectListItem { Value = "US", Text = "USA"},
new SelectListItem { Value = "UK", Text = "United Kingdom"}, new SelectListItem { Value = "UK", Text = "United Kingdom"},
new SelectListItem { Value = "RU", Text = "Russia"} new SelectListItem { Value = "RU", Text = "Russia"}
}; };
public void OnGet() public void OnGet()
{ {
MyDetailedModel = new DetailedModel MyDetailedModel = new DetailedModel
{ {
ComboCarType = CarType.Coupe, ComboCarType = CarType.Coupe,
RadioCarType = CarType.Sedan, RadioCarType = CarType.Sedan,
ComboCountry = "RU", ComboCountry = "RU",
RadioCountry = "UK" RadioCountry = "UK"
}; };
} }
public class DetailedModel public class DetailedModel
{ {
public CarType ComboCarType { get; set; } public CarType ComboCarType { get; set; }
[AbpRadioButton(Inline = true)] [AbpRadioButton(Inline = true)]
public CarType RadioCarType { get; set; } public CarType RadioCarType { get; set; }
[SelectItems(nameof(CountryList))] [SelectItems(nameof(CountryList))]
public string ComboCountry { get; set; } public string ComboCountry { get; set; }
[AbpRadioButton()] [AbpRadioButton()]
[SelectItems(nameof(CountryList))] [SelectItems(nameof(CountryList))]
public string RadioCountry { get; set; } public string RadioCountry { get; set; }
} }
public enum CarType public enum CarType
{ {
Sedan, Sedan,
Hatchback, Hatchback,
StationWagon, StationWagon,
Coupe Coupe
}
} }
}
```` ````
As you see in example above: As you see in example above:
@ -265,13 +270,6 @@ By default, it will try to find "DisplayName:{PropertyName}" or "{PropertyName}"
You can set it yourself by using `[Display()]` attribute of Asp.Net Core. You can use a localization key in this attribute. See example below: You can set it yourself by using `[Display()]` attribute of Asp.Net Core. You can use a localization key in this attribute. See example below:
````csharp ````csharp
[Display(Name = "Name")] [Display(Name = "Name")]
public string Name { get; set; } public string Name { get; set; }
```` ````

2
docs/en/Unit-Of-Work.md

@ -325,7 +325,7 @@ public async Task<int> CreateAsync(string name)
If your intent is just to save the changes after creating/updating/deleting an entity, it is suggested to use the `autoSave` option instead of manually using the `CurrentUnitOfWork.SaveChangesAsync()`. If your intent is just to save the changes after creating/updating/deleting an entity, it is suggested to use the `autoSave` option instead of manually using the `CurrentUnitOfWork.SaveChangesAsync()`.
> **Note-1**: All changes are automatically saved when a unit of work ends without any error. So, don't call `SaveChangesAsync()` unless you really need it. > **Note-1**: All changes are automatically saved when a unit of work ends without any error. So, don't call `SaveChangesAsync()` and don't set `autoSave` to `true` unless you really need it.
> >
> **Note-2**: If you use `Guid` as the primary key, you never need to save changes on insert to just get the generated id, because `Guid` keys are set in the application and are immediately available once you create a new entity. > **Note-2**: If you use `Guid` as the primary key, you never need to save changes on insert to just get the generated id, because `Guid` keys are set in the application and are immediately available once you create a new entity.

180
docs/en/Virtual-File-System.md

@ -1,40 +1,30 @@
## Virtual File System # Virtual File System
The Virtual File System makes it possible to manage files that do not physically exist on the file system (disk). It's mainly used to embed (js, css, image..) files into assemblies and use them like physical files at runtime. The Virtual File System makes it possible to manage files that do not physically exist on the file system (disk). It's mainly used to embed (js, css, image..) files into assemblies and use them like physical files at runtime.
### Volo.Abp.VirtualFileSystem Package ## Installation
Volo.Abp.VirtualFileSystem is the core package of the virtual file system. Install it in your project using the package manager console (PMC): > Most of the times you don't need to manually install this package since it comes pre-installed with the [application startup template](Startup-Templates/Application.md).
``` [Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) is the main page of the Virtual File System.
Install-Package Volo.Abp.VirtualFileSystem
```
> This package is already installed by default with the startup template. So, most of the time, you will not need to install it manually. Use the ABP CLI to add this package to your project:
Then you can add **AbpVirtualFileSystemModule** dependency to your module: * Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), if you haven't installed it.
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.VirtualFileSystem` package.
* Run `abp add-package Volo.Abp.VirtualFileSystem` command.
```c# If you want to do it manually, install the [Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) NuGet package to your project and add `[DependsOn(typeof(AbpVirtualFileSystemModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project.
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace MyCompany.MyProject ## Working with the Embedded Files
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
//...
}
}
```
#### Registering Embedded Files ### Embed the Files
A file should be first marked as an embedded resource to embed the file into the assembly. The easiest way to do it is to select the file from the **Solution Explorer** and set **Build Action** to **Embedded Resource** from the **Properties** window. Example: A file should be first marked as an **embedded resource** to embed the file into the assembly. The easiest way to do it is to select the file from the **Solution Explorer** and set **Build Action** to **Embedded Resource** from the **Properties** window. Example:
![build-action-embedded-resource-sample](images/build-action-embedded-resource-sample.png) ![build-action-embedded-resource-sample](images/build-action-embedded-resource-sample.png)
If you want to add multiple files, this can be tedious. Alternatively, you can directly edit your **.csproj** file: If you want to add multiple files, this can be tedious. Alternatively, you can directly edit your `.csproj` file:
````C# ````C#
<ItemGroup> <ItemGroup>
@ -45,76 +35,60 @@ If you want to add multiple files, this can be tedious. Alternatively, you can d
This configuration recursively adds all files under the **MyResources** folder of the project (including the files you will add in the future). This configuration recursively adds all files under the **MyResources** folder of the project (including the files you will add in the future).
Then the module needs to be configured using `AbpVirtualFileSystemOptions` to register the embedded files to the virtual file system. Example: Embedding a file in the project/assembly may cause problems if a file name contains some special chars. To overcome this limitation;
````C# 1. Add [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet package to the project that contains the embedded resource(s).
using Microsoft.Extensions.DependencyInjection; 2. Add `<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>` into the `<PropertyConfig>...</PropertyConfig>` section of your `.csproj` file.
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace MyCompany.MyProject > While these two steps are optional and ABP can work without these configuration, it is strongly suggested to make it.
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
//Register all embedded files of this assembly to the virtual file system
options.FileSets.AddEmbedded<MyModule>("YourRootNameSpace");
});
//... ### Configure the AbpVirtualFileSystemOptions
}
}
}
````
The `AddEmbedded` extension method takes a class, finds all embedded files from the assembly of the given class and registers them to the virtual file system. More concisely it could be written as follows: Use `AbpVirtualFileSystemOptions` [options class](Options.md) to register the embedded files to the virtual file system in the `ConfigureServices` method of your [module](Module-Development-Basics.md).
````C# **Example: Add embedded files to the virtual file system**
options.FileSets.Add(
new EmbeddedFileSet(typeof(MyModule).Assembly), "YourRootNameSpace"); ````csharp
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyModule>();
});
```` ````
> "YourRootNameSpace" is the root namespace of your project. It can be empty if your root namespace is empty. The `AddEmbedded` extension method takes a class, finds all embedded files from the **assembly of the given class** and registers them to the virtual file system. It is common to pass the module class as the generic argument.
#### Getting Virtual Files: IVirtualFileProvider `AddEmbedded` can get two optional parameters;
After embedding a file into an assembly and registering it to the virtual file system, the `IVirtualFileProvider` interface can be used to get files or directory contents: * `baseNamespace`: This may only needed if you didn't configure the `GenerateEmbeddedFilesManifest` step explained above and your root namespace is not empty. In this case, set your root namespace here.
* `baseFolder`: If you don't want to expose all embedded files in the project, but only want to expose a specific folder (and sub folders/files), then you can set the base folder relative to your project root page.
````C# **Example: Add files under the `MyFiles` folder in the project**
public class MyService
````csharp
Configure<AbpVirtualFileSystemOptions>(options =>
{ {
private readonly IVirtualFileProvider _virtualFileProvider; options.FileSets.AddEmbedded<MyModule>(
baseNamespace: "Acme.BookStore.MyFiles",
baseFolder: "/MyFiles"
);
});
````
public MyService(IVirtualFileProvider virtualFileProvider) This example assumes;
{
_virtualFileProvider = virtualFileProvider;
}
public void Foo() * Your project root (default) namespace is `Acme.BookStore`.
{ * Your project has a folder, named `MyFiles`
//Getting a single file * You only want to add `MyFiles` folder to the virtual file system.
var file = _virtualFileProvider.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString(); //ReadAsString is an extension method of ABP
//Getting all files/directories under a directory ### Dealing With Embedded Files During Development
var directoryContents = _virtualFileProvider.GetDirectoryContents("/MyResources/js");
}
}
````
#### Dealing With Embedded Files During Development Embedding a file into an assembly and being able to use it from another project just by referencing the assembly (or adding a NuGet package) is invaluable for creating a re-usable module. However, it makes it a little bit harder to develop the module itself.
Embedding a file into a module assembly and being able to use it from another project just by referencing the assembly (or adding a nuget package) is invaluable for creating a re-usable module. However, it does make it a little bit harder to develop the module itself.
Let's assume that you're developing a module that contains an embedded JavaScript file. Whenever you change this file you must re-compile the project, re-start the application and refresh the browser page to take the change. Obviously, this is very time consuming and tedious. Let's assume that you're developing a module that contains an embedded JavaScript file. Whenever you change this file you must re-compile the project, re-start the application and refresh the browser page to take the change. Obviously, this is very time consuming and tedious.
What is needed is the ability for the application to directly use the physical file at development time and a have a browser refresh reflect any change made in the JavaScript file. The `ReplaceEmbeddedByPhysical` method makes all this possible. What is needed is the ability for the application to directly use the physical file at development time and a have a browser refresh reflect any change made in the JavaScript file. The `ReplaceEmbeddedByPhysical` method makes all this possible.
The example below shows an application that depends on a module (`MyModule`) that itself contains embedded files. The application can reach the source code of the module at development time. The example below shows an application that depends on a module (`MyModule`) that itself contains embedded files. The application can reach the source code of the module at development time.
````C# ````C#
[DependsOn(typeof(MyModule))] [DependsOn(typeof(MyModule))]
@ -128,29 +102,63 @@ public class MyWebAppModule : AbpModule
{ {
Configure<AbpVirtualFileSystemOptions>(options => Configure<AbpVirtualFileSystemOptions>(options =>
{ {
//ReplaceEmbeddedByPhysical gets the root folder of the MyModule project
options.FileSets.ReplaceEmbeddedByPhysical<MyModule>( options.FileSets.ReplaceEmbeddedByPhysical<MyModule>(
Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}MyModuleProject", Path.DirectorySeparatorChar)) Path.Combine(
hostingEnvironment.ContentRootPath,
string.Format(
"..{0}MyModuleProject",
Path.DirectorySeparatorChar
)
)
); );
}); });
} }
//...
} }
} }
```` ````
The code above assumes that `MyWebAppModule` and `MyModule` are two different projects in a Visual Studio solution and `MyWebAppModule` depends on the `MyModule`. The code above assumes that `MyWebAppModule` and `MyModule` are two different projects in a Visual Studio solution and `MyWebAppModule` depends on the `MyModule`.
### ASP.NET Core Integration > The [application startup template](Startup-Templates/Application.md) already uses this technique for the localization files. So, when you change a localization file it automatically detects the change.
## IVirtualFileProvider
After embedding a file into an assembly and registering it to the virtual file system, the `IVirtualFileProvider` interface can be used to get files or directory contents:
````C#
public class MyService
{
private readonly IVirtualFileProvider _virtualFileProvider;
public MyService(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
}
public void Foo()
{
//Getting a single file
var file = _virtualFileProvider
.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString();
//Getting all files/directories under a directory
var directoryContents = _virtualFileProvider
.GetDirectoryContents("/MyResources/js");
}
}
````
## ASP.NET Core Integration
The Virtual File System is well integrated to ASP.NET Core: The Virtual File System is well integrated to ASP.NET Core:
* Virtual files can be used just like physical (static) files in a web application. * Virtual files can be used just like physical (static) files in a web application.
* Js, css, image files and all other web content types can be embedded into assemblies and used just like the physical files. * Js, css, image files and all other web content types can be embedded into assemblies and used just like the physical files.
* An application (or another module) can override a virtual file of a module just like placing a file with the same name and extension into the same folder of the virtual file. * An application (or another module) can **override a virtual file** of a module just like placing a file with the same name and extension into the same folder of the virtual file.
#### Virtual Files Middleware ### UseVirtualFiles Middleware
The Virtual Files Middleware is used to serve embedded (js, css, image...) files to clients/browsers just like physical files in the **wwwroot** folder. Add it just after the static file middleware as shown below: The Virtual Files Middleware is used to serve embedded (js, css, image...) files to clients/browsers just like physical files in the **wwwroot** folder. Add it just after the static file middleware as shown below:
@ -160,4 +168,14 @@ app.UseVirtualFiles();
Adding virtual files middleware after the static files middleware makes it possible to override a virtual file with a real physical file simply by placing it in the same location as the virtual file. Adding virtual files middleware after the static files middleware makes it possible to override a virtual file with a real physical file simply by placing it in the same location as the virtual file.
>The Virtual File Middleware only serves the virtual wwwroot folder contents - just like the other static files. > `UseVirtualFiles()` is already configured for the [application startup template](Startup-Templates/Application.md).
#### Static Virtual File Folders
By default, ASP.NET Core only allows the `wwwroot` folder to contain the static files consumed by the clients. When you use the `UseVirtualFiles` middleware, the following folders also can contain static files:
* Pages
* Views
* Themes
This allows to add `.js`, `.css`... files near to your `.cshtml` file that is easier to develop and maintain your project.

4
docs/en/docs-nav.json

@ -366,6 +366,10 @@
{ {
"text": "Angular", "text": "Angular",
"items": [ "items": [
{
"text": "Migration Guide v2.x to v3",
"path": "UI/Angular/Migration-Guide-v3.md"
},
{ {
"text": "Service Proxies", "text": "Service Proxies",
"path": "UI/Angular/Service-Proxies.md" "path": "UI/Angular/Service-Proxies.md"

3
docs/zh-Hans/CLI.md

@ -169,7 +169,8 @@ abp update [options]
* `--nuget`: 仅更新的NuGet包 * `--nuget`: 仅更新的NuGet包
* `--solution-path` 或 `-sp`: 指定解决方案路径/目录. 默认使用当前目录 * `--solution-path` 或 `-sp`: 指定解决方案路径/目录. 默认使用当前目录
* `--solution-name` 或 `-sn`: 指定解决方案名称. 默认在目录中搜索`*.sln`文件. * `--solution-name` 或 `-sn`: 指定解决方案名称. 默认在目录中搜索`*.sln`文件.
*`--check-all`: 分别检查每个包的新版本. 默认是 `false`.
### 切换到每晚构建(预览)包 ### 切换到每晚构建(预览)包
想要切换到ABP框架的最新预览版可以使用此命令. 想要切换到ABP框架的最新预览版可以使用此命令.

4
docs/zh-Hans/Entity-Framework-Core-MySQL.md

@ -8,14 +8,14 @@
## 替换模块依赖项 ## 替换模块依赖项
在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreMySQLModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.MySQL;`). 在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreMySQLModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.MySQL;`).
## UseMySQL() ## UseMySQL()
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseMySQL()`. 检查下列文件: 查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseMySQL()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. * `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. > 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.

60
docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md

@ -0,0 +1,60 @@
# 切换到 EF Core Oracle Devart 提供程序
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,有关其他选项,请参见[文档](Entity-Framework-Core-Oracle.md).
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包
解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的[Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包.
## 替换模块依赖项
在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`).
## UseOracle()
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
找到 *YourProjectName*MigrationsDbContextFactory.cs 的 `CreateDbContext()` 方法,将以下代码块
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
替换为:
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.
## 更改连接字符串
Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关Oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/).
通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构.
## 重新生成迁移
启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败.
* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案.
* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目).
这将创建一个配置所有数据库对象(表)的数据库迁移.
运行 `.DbMigrator` 项目创建数据库和初始种子数据.
## 运行应用程序
它已准备就绪, 只需要运行该应用程序与享受编码.

67
docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md

@ -0,0 +1,67 @@
# 切换到EF Core Oracle提供程序
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包
解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包.
## 替换模块依赖项
在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`).
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法:
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
与这个
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.
## 更改连接字符串
Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/).
通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构.
## 重新生成迁移
启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败.
* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案.
* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目).
这将创建一个配置所有数据库对象(表)的数据库迁移.
运行 `.DbMigrator` 项目创建数据库和初始种子数据.
## 运行应用程序
它已准备就绪, 只需要运行该应用程序与享受编码.

67
docs/zh-Hans/Entity-Framework-Core-Oracle.md

@ -1,67 +1,10 @@
# 切换到EF Core Oracle提供程序 # 切换到EF Core Oracle 提供程序
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库 ABP框架提供了两种不同的Oracle包集成. 你可以选择以下其中一个:
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 * **[Volo.Abp.EntityFrameworkCore.Oracle](Entity-Framework-Core-Oracle-Official.md)** 使用官方 & 免费的oracle驱动 ( **当前处于 beta**).
* **[Volo.Abp.EntityFrameworkCore.Oracle.Devart](Entity-Framework-Core-Oracle-Devart.md)** 使用[Devart](https://www.devart.com/)公司提供的商业(付费)驱动.
解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包. > 你可以选择一个你想要的包,如果你不知道它们之间的区别,请在网站上进行搜索. ABP框架仅提供集成,不提供第三库类库的支持.
## 替换模块依赖项
在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`).
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法:
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
与这个
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.
## 更改连接字符串
Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/).
通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构.
## 重新生成迁移
启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败.
* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案.
* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目).
这将创建一个配置所有数据库对象(表)的数据库迁移.
运行 `.DbMigrator` 项目创建数据库和初始种子数据.
## 运行应用程序
它已准备就绪, 只需要运行该应用程序与享受编码.

4
docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md

@ -8,14 +8,14 @@
## 替换模块依赖项 ## 替换模块依赖项
在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). 在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.PostgreSql;`).
## UseNpgsql() ## UseNpgsql()
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseNpgsql()`. 检查下列文件: 查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseNpgsql()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. * `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. > 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.

4
docs/zh-Hans/Entity-Framework-Core-SQLite.md

@ -8,14 +8,14 @@
## 替换模块依赖项 ## 替换模块依赖项
在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreSqliteModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Sqlite;`). 在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreSqliteModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Sqlite;`).
## UseSqlite() ## UseSqlite()
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseSqlite()`. 检查下列文件: 查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseSqlite()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. * `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. > 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.

224
docs/zh-Hans/Tutorials/Part-1.md

@ -737,8 +737,8 @@ $(function () {
是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具: 是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具:
- [Angular CLI](https://angular.io/cli) 用于创建模块,组件和服务. - [Angular CLI](https://angular.io/cli) 用于创建模块,组件和服务.
- [NGXS](https://ngxs.gitbook.io/ngxs/) 用于管理状态库.
- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库. - [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库.
- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) 用做 datatable 类库.
- [Visual Studio Code](https://code.visualstudio.com/) 用做代码编辑器 (你可以选择自己喜欢的编辑器). - [Visual Studio Code](https://code.visualstudio.com/) 用做代码编辑器 (你可以选择自己喜欢的编辑器).
#### 安装 NPM 包 #### 安装 NPM 包
@ -761,26 +761,56 @@ yarn ng generate module book --routing true
#### 路由 #### 路由
打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的 `import` 和路由: 打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的路由:
```js ```js
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <== const routes: Routes = [
// ...
// added a new route to the routes array
{
path: 'books',
loadChildren: () => import('./book/book.module').then(m => m.BookModule)
}
]
```
//...added book path with the below to the routes array * 我们添加了一个懒加载路由. 参阅 [嬾加載功能模块](https://angular.io/guide/lazy-loading-ngmodules#lazy-loading-feature-modules).
{
path: 'book', 打开位于 `src\app` 目录下的 `route.provider.ts` 文件,用以下内容替换它:
component: ApplicationLayoutComponent,
loadChildren: () => import('./book/book.module').then(m => m.BookModule), ```js
data: { import { RoutesService, eLayoutType } from '@abp/ng.core';
routes: { import { APP_INITIALIZER } from '@angular/core';
name: '::Menu:Book',
iconClass: 'fas fa-book' export const APP_ROUTE_PROVIDER = [
} as ABP.Route { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
}, ];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
//...
// added below element
{
path: '/books',
name: '::Menu:Books',
iconClass: 'fas fa-book',
order: 101,
layout: eLayoutType.application,
},
]);
};
} }
``` ```
* `ApplicationLayoutComponent` 配置将应用程序布局设置为新页面, 我们添加了 `data` 对象. `name` 是菜单项的名称,`iconClass` 是菜单项的图标. * 我们添加了一个新的路由元素在菜单上显示为 "Books" 的导航元素.
* `path` 路由的URL.
* `name` 菜单项的名称,可以使用本地化Key.
* `iconClass` 菜单项的图标.
* `order` 菜单项的排序.我们定义了101,它显示在 "Administration" 项的后面.
* `layout` BooksModule路由的布局. 可以定义 `eLayoutType.application`, `eLayoutType.account` 或 `eLayoutType.empty`.
更多信息请参阅[RoutesService 文档](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice).
#### Book 列表组件 #### Book 列表组件
@ -848,35 +878,6 @@ yarn start
![Initial book list page](./images/bookstore-initial-book-list-page.png) ![Initial book list page](./images/bookstore-initial-book-list-page.png)
#### 创建 BookState
运行以下命令创建名为 `BookState` 的新state:
```bash
npx @ngxs/cli --name book --directory src/app/book
```
* 此命令在 `src/app/book/state` 文件夹下创建了 `book.state.ts` 和 `book.actions.ts` 文件. 参阅 [NGXS CLI文档](https://www.ngxs.io/plugins/cli)了解更多.
将 `BookState` 导入到 `src/app` 文件夹中的 `app.module.ts` 中. 然后添加 `BookState` 到 `NgxsModule` 的 `forRoot` 静态方法,作为该方法的第一个参数的数组元素.
```js
// ...
import { BookState } from './book/state/book.state'; //<== imported BookState ==>
@NgModule({
imports: [
// other imports
NgxsModule.forRoot([BookState]), //<== added BookState ==>
//other imports
],
// ...
})
export class AppModule {}
```
#### 生成代理 #### 生成代理
ABP CLI提供了 `generate-proxy` 命令为你的服务HTTP API生成客户端代理简化客户端使用服务的成本. 运行 `generate-proxy` 命令前你的host必须正在运行. 参阅 [CLI 文档](../CLI.md). ABP CLI提供了 `generate-proxy` 命令为你的服务HTTP API生成客户端代理简化客户端使用服务的成本. 运行 `generate-proxy` 命令前你的host必须正在运行. 参阅 [CLI 文档](../CLI.md).
@ -893,109 +894,41 @@ abp generate-proxy --module app
![Generated files](./images/generated-proxies.png) ![Generated files](./images/generated-proxies.png)
#### GetBook 动作
动作可以被认为是一个命令,它应该触发某些事情发生,或者是已经发生的事情的结果事件.[See NGXS Actions文档](https://www.ngxs.io/concepts/actions).
打开 `app/book/state` 目录下的 `book.actions.ts` 文件用以下内容替换它:
```js
export class GetBook {
static readonly type = '[Book] Get';
}
```
#### 实现 BookState
打开 `app/book/state` 目录下的 `book.state.ts` 文件用以下内容替换它:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks } from './book.actions';
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
book: booksResponse,
});
})
);
}
}
```
* 我们添加了book属性到BookStateModel模态框.
* 我们添加了 `GetBook` 动作. 它通过 ABP CLI生成的 `BookService` 检索图书数据.
* `NGXS` 需要在不订阅get函数的情况下返回被观察对象.
#### BookListComponent #### BookListComponent
打开 `app\book\book-list` 目录下的 `book-list.component.ts` 用以下内容替换它: 打开 `app\book\book-list` 目录下的 `book-list.component.ts` 用以下内容替换它:
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions'; import { BookService } from '../services';
import { BookState } from '../state/book.state';
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [ListService],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
books$: Observable<BookDto[]>;
booksType = BookType; booksType = BookType;
loading = false; constructor(public readonly list: ListService, private bookService: BookService) {}
constructor(private store: Store) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
} }
``` ```
* 我们添加了 `get` 函数获取book更新store. * 我们注入了生成的 `BookService`.
* 有关 `NGXS` 特性的更多信息请参见NGXS文档中的[Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions)和[Select](https://ngxs.gitbook.io/ngxs/concepts/select). * 我们实现了 [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service),它是一个公用服务,提供了简单的分页,排序和搜索.
打开 `app\book\book-list` 目录下的 `book-list.component.html` 用以下内容替换它: 打开 `app\book\book-list` 目录下的 `book-list.component.html` 用以下内容替换它:
@ -1005,38 +938,31 @@ export class BookListComponent implements OnInit {
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<h5 class="card-title"> <h5 class="card-title">
{%{{{ "::Menu:Book" | abpLocalization }}}%} {%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5> </h5>
</div> </div>
<div class="text-right col col-md-6"></div> <div class="text-right col col-md-6"></div>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<abp-table <ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
[value]="book$ | async" <ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
[abpLoading]="loading" <ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
[headerTemplate]="tableHeader" <ng-template let-row="row" ngx-datatable-cell-template>
[bodyTemplate]="tableBody" {%{{{ booksType[row.type] }}}%}
[rows]="10" </ng-template>
[scrollable]="true" </ngx-datatable-column>
> <ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
</abp-table> <ng-template let-row="row" ngx-datatable-cell-template>
<ng-template #tableHeader> {%{{{ row.publishDate | date }}}%}
<tr> </ng-template>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th> </ngx-datatable-column>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th> <ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th> <ng-template let-row="row" ngx-datatable-cell-template>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th> {%{{{ row.price | currency }}}%}
</tr> </ng-template>
</ng-template> </ngx-datatable-column>
<ng-template #tableBody let-data> </ngx-datatable>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ bookType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
</div> </div>
</div> </div>
``` ```

596
docs/zh-Hans/Tutorials/Part-2.md

@ -454,78 +454,48 @@ $(function () {
下面的章节中,你将学习到如何创建一个新的模态对话框来新增Book实体. 下面的章节中,你将学习到如何创建一个新的模态对话框来新增Book实体.
#### 状态定义 #### 添加 modal 到 BookListComponent
在 `app\book\state` 文件夹下打开 `book.action.ts` 文件,使用以下内容替换它:
```js Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
import { CreateUpdateBookDto } from '../models'; //<== added this line ==>
export class GetBooks {
static readonly type = '[Book] Get';
}
// added CreateUpdateBook class ```js
export class CreateUpdateBook { import { ListService, PagedResultDto } from '@abp/ng.core';
static readonly type = '[Book] Create Update Book'; import { Component, OnInit } from '@angular/core';
constructor(public payload: CreateUpdateBookDto) { } import { BookDto, BookType } from '../models';
} import { BookService } from '../services';
```
* 我们导入了 `CreateUpdateBookDto` 模型并且创建了 `CreateUpdateBook` 动作. @Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [ListService],
})
export class BookListComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换它: booksType = BookType;
```js isModalOpen = false; // <== added this line ==>
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel { constructor(public readonly list: ListService, private bookService: BookService) {}
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({ ngOnInit() {
name: 'BookState', const bookStreamCreator = (query) => this.bookService.getListByInput(query);
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {} this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
@Action(GetBooks) });
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((bookResponse) => {
ctx.patchState({
book: bookResponse,
});
})
);
} }
// added CreateUpdateBook action listener // added createBook method
@Action(CreateUpdateBook) createBook() {
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) { this.isModalOpen = true;
return this.bookService.createByInput(action.payload);
} }
} }
``` ```
* 我们导入了 `CreateUpdateBook` 动作并且定义了 `save` 方法监听 `CreateUpdateBook` 动作去创建图书. * 我们定义了一个名为 `isModalOpen` 的变量和 `createBook` 方法.
当 `SaveBook` 动作被分派时,save方法被执行. 它调用 `BookService` 的 `createByInput` 方法.
#### 添加模态到 BookListComponent
打开 `app\book\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它: 打开 `app\book\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它:
@ -534,19 +504,12 @@ export class BookState {
<div class="card-header"> <div class="card-header">
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<h5 class="card-title"> <h5 class="card-title">{%{{{ '::Menu:Books' | abpLocalization }}}%}</h5>
{%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5>
</div> </div>
<!--Added new book button --> <!--Added new book button -->
<div class="text-right col col-md-6"> <div class="text-right col col-md-6">
<div class="text-lg-right pt-2"> <div class="text-lg-right pt-2">
<button <button id="create" class="btn btn-primary" type="button" (click)="createBook()">
id="create"
class="btn btn-primary"
type="button"
(click)="createBook()"
>
<i class="fa fa-plus mr-1"></i> <i class="fa fa-plus mr-1"></i>
<span>{%{{{ "::NewBook" | abpLocalization }}}%}</span> <span>{%{{{ "::NewBook" | abpLocalization }}}%}</span>
</button> </button>
@ -555,47 +518,40 @@ export class BookState {
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<abp-table <ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
[value]="book$ | async" <ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
[abpLoading]="loading" <ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
[headerTemplate]="tableHeader" <ng-template let-row="row" ngx-datatable-cell-template>
[bodyTemplate]="tableBody" {%{{{ booksType[row.type] }}}%}
[rows]="10" </ng-template>
[scrollable]="true" </ngx-datatable-column>
> <ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
</abp-table> <ng-template let-row="row" ngx-datatable-cell-template>
<ng-template #tableHeader> {%{{{ row.publishDate | date }}}%}
<tr> </ng-template>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th> </ngx-datatable-column>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th> <ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th> <ng-template let-row="row" ngx-datatable-cell-template>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th> {%{{{ row.price | currency }}}%}
</tr> </ng-template>
</ng-template> </ngx-datatable-column>
<ng-template #tableBody let-data> </ngx-datatable>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ bookType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
</div> </div>
</div> </div>
<!--added modal--> <!--added modal-->
<abp-modal [(visible)]="isModalOpen"> <abp-modal [(visible)]="isModalOpen">
<ng-template #abpHeader> <ng-template #abpHeader>
<h3>{%{{{ '::NewBook' | abpLocalization }}}%}</h3> <h3>{%{{{ '::NewBook' | abpLocalization }}}%}</h3>
</ng-template> </ng-template>
<ng-template #abpBody> </ng-template> <ng-template #abpBody> </ng-template>
<ng-template #abpFooter> <ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose> <button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpAccount::Close' | abpLocalization }}}%} {%{{{ 'AbpAccount::Close' | abpLocalization }}}%}
</button> </button>
</ng-template> </ng-template>
</abp-modal> </abp-modal>
``` ```
@ -603,55 +559,6 @@ export class BookState {
* `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处. * `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处.
* 我们添加了 `New book` 按钮到 `AbpContentToolbar`. * 我们添加了 `New book` 按钮到 `AbpContentToolbar`.
打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
loading = false;
isModalOpen = false; // <== added this line ==>
constructor(private store: Store) {}
ngOnInit() {
this.get();
}
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
}
// added createBook method
createBook() {
this.isModalOpen = true;
}
}
```
* 我们添加了 `isModalOpen = false` 和 `createBook` 方法.
你可以打开浏览器,点击**New book**按钮看到模态框. 你可以打开浏览器,点击**New book**按钮看到模态框.
![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) ![Empty modal for new book](./images/bookstore-empty-new-book-modal.png)
@ -660,51 +567,46 @@ export class BookListComponent implements OnInit {
[响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入. [响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入.
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: 打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions'; import { BookService } from '../services';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==> import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [ListService],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
books$: Observable<BookDto[]>;
booksType = BookType; booksType = BookType;
loading = false;
isModalOpen = false; isModalOpen = false;
form: FormGroup; // <== added this line ==> form: FormGroup; // <== added this line ==>
constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==> constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder // <== injected FormBuilder ==>
) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
this.buildForm(); //<== added this line ==> this.buildForm(); // <== added this line ==>
this.isModalOpen = true; this.isModalOpen = true;
} }
@ -729,7 +631,7 @@ export class BookListComponent implements OnInit {
#### 创建表单的DOM元素 #### 创建表单的DOM元素
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpBody> </ng-template>`: 打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpBody> </ng-template>`:
```html ```html
<ng-template #abpBody> <ng-template #abpBody>
@ -798,16 +700,13 @@ export class BooksModule { }
* 我们导入了 `NgbDatepickerModule` 来使用日期选择器. * 我们导入了 `NgbDatepickerModule` 来使用日期选择器.
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: 打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions'; import { BookService } from '../services';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==> import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==>
@ -815,37 +714,34 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==> providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added a provide ==>
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
books$: Observable<BookDto[]>;
booksType = BookType; booksType = BookType;
//added bookTypeArr array // <== added bookTypeArr array ==>
bookTypeArr = Object.keys(BookType).filter( bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false;
isModalOpen = false; isModalOpen = false;
form: FormGroup; form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) {} constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -882,16 +778,13 @@ export class BookListComponent implements OnInit {
#### 保存图书 #### 保存图书
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: 打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==> import { BookService } from '../services';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -899,11 +792,10 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
books$: Observable<BookDto[]>;
booksType = BookType; booksType = BookType;
@ -911,24 +803,22 @@ export class BookListComponent implements OnInit {
(bookType) => typeof this.booksType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false;
isModalOpen = false; isModalOpen = false;
form: FormGroup; form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) {} constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -951,19 +841,18 @@ export class BookListComponent implements OnInit {
return; return;
} }
this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { this.bookService.createByInput(this.form.value).subscribe(() => {
this.isModalOpen = false; this.isModalOpen = false;
this.form.reset(); this.form.reset();
this.get(); this.list.get();
}); });
} }
} }
``` ```
* 我们导入了 `CreateUpdateBook`.
* 我们添加了 `save` 方法. * 我们添加了 `save` 方法.
打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list`文件, 添加 `abp-button` 保存图书. 打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件, 找到 `<ng-template #abpFooter>` 元素,使用下面元素替换它:
```html ```html
<ng-template #abpFooter> <ng-template #abpFooter>
@ -994,90 +883,49 @@ export class BookListComponent implements OnInit {
### 更新图书 ### 更新图书
#### CreateUpdateBook 动作 打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件并且添加名为 `selectedBook` 的变量.
打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件,使用以下内容替换它:
```js
import { CreateUpdateBookDto } from '../models';
export class GetBooks {
static readonly type = '[Book] Get';
}
export class CreateUpdateBook {
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==>
}
```
* 我们在 `CreateUpdateBook` 动作的构造函数添加了 `id` 参数.
打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换 `save` 方法:
```js
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
return this.bookService.createByInput(action.payload);
}
}
```
#### BookListComponent
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,在构造函数注入 `BookService` 服务,并添加 名为 `selectedBook` 的变量.
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models'; import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions'; import { BookService } from '../services';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { BookService } from '../services'; // <== imported BookService ==>
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BookState.getBooks) book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
book$: Observable<BookDto[]>;
bookType = BookType; booksType = BookType;
bookTypeArr = Object.keys(BookType).filter( bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.bookType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false;
isModalOpen = false; isModalOpen = false;
form: FormGroup; form: FormGroup;
selectedBook = {} as BookDto; // <== declared selectedBook ==> selectedBook = {} as BookDto; // <== declared selectedBook ==>
constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==> constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() { ngOnInit() {
this.get(); const bookStreamCreator = (query) => this.bookService.getListByInput(query);
}
get() { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.loading = true; this.book = response;
this.store });
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
// <== this method is replaced ==> // <== this method is replaced ==>
@ -1109,58 +957,46 @@ export class BookListComponent implements OnInit {
}); });
} }
// <== this method is replaced ==>
save() { save() {
if (this.form.invalid) { if (this.form.invalid) {
return; return;
} }
//<== added this.selectedBook.id ==> // <== added request ==>
this.store const request = this.selectedBook.id
.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) ? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id)
.subscribe(() => { : this.bookService.createByInput(this.form.value);
this.isModalOpen = false;
this.form.reset(); request.subscribe(() => {
this.get(); this.isModalOpen = false;
}); this.form.reset();
this.list.get();
});
} }
} }
``` ```
* 我们导入了 `BookService`.
* 我们声明了类型为 `BookDto` 的 `selectedBook` 变量. * 我们声明了类型为 `BookDto` 的 `selectedBook` 变量.
* 我们在构造函数注入了 `BookService`, 它用于检索正在编辑的图书数据.
* 我们添加了 `editBook` 方法, 根据给定图书 `Id` 设置 `selectedBook` 对象. * 我们添加了 `editBook` 方法, 根据给定图书 `Id` 设置 `selectedBook` 对象.
* 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单. * 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单.
* 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象. * 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象.
* 我们在 `CreateUpdateBook` 构造函数添加了 `selectedBook.id`. * 我们替换了 `save` 方法.
#### 添加 "Actions" 下拉框到表格 #### 添加 "Actions" 下拉框到表格
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<div class="card-body">` 标签: 打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<div class="card-body">` 标签:
```html ```html
<div class="card-body"> <div class="card-body">
<abp-table <ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
[value]="book$ | async" <!-- added actions column -->
[abpLoading]="loading" <ngx-datatable-column
[headerTemplate]="tableHeader" [name]="'::Actions' | abpLocalization"
[bodyTemplate]="tableBody" [maxWidth]="150"
[rows]="10" [sortable]="false"
[scrollable]="true" >
> <ng-template let-row="row" ngx-datatable-cell-template>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{%{{{ "::Actions" | abpLocalization }}}%}</th>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th>
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>
<div ngbDropdown container="body" class="d-inline-block"> <div ngbDropdown container="body" class="d-inline-block">
<button <button
class="btn btn-primary btn-sm dropdown-toggle" class="btn btn-primary btn-sm dropdown-toggle"
@ -1168,25 +1004,38 @@ export class BookListComponent implements OnInit {
aria-haspopup="true" aria-haspopup="true"
ngbDropdownToggle ngbDropdownToggle
> >
<i class="fa fa-cog mr-1"></i>{%{{{ "::Actions" | abpLocalization }}}%} <i class="fa fa-cog mr-1"></i>{%{{{ '::Actions' | abpLocalization }}}%}
</button> </button>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<button ngbDropdownItem (click)="editBook(data.id)"> <button ngbDropdownItem (click)="editBook(row.id)">
{%{{{ "::Edit" | abpLocalization }}}%} {%{{{ '::Edit' | abpLocalization }}}%}
</button> </button>
</div> </div>
</div> </div>
</td> </ng-template>
<td>{%{{{ data.name }}}%}</td> </ngx-datatable-column>
<td>{%{{{ bookType[data.type] }}}%}</td> <ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<td>{%{{{ data.publishDate | date }}}%}</td> <ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<td>{%{{{ data.price }}}%}</td> <ng-template let-row="row" ngx-datatable-cell-template>
</tr> {%{{{ booksType[row.type] }}}%}
</ng-template> </ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.publishDate | date }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.price | currency }}}%}
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</div> </div>
``` ```
- 我们添加了 "Actions" 栏的 `th`. - 我们添加了 "Actions" 栏的 `th`.
- 我们为 "Actions" 栏添加了 `ngx-datatable-column`.
- 我们添加了带有 `ngbDropdownToggle` 的 `button`,在点击按钮时打开操作. - 我们添加了带有 `ngbDropdownToggle` 的 `button`,在点击按钮时打开操作.
- 我们习惯于将[NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples)用于操作的下拉菜单. - 我们习惯于将[NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples)用于操作的下拉菜单.
@ -1194,7 +1043,7 @@ UI最终看起来像这样:
![Action buttons](./images/bookstore-actions-buttons.png) ![Action buttons](./images/bookstore-actions-buttons.png)
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpHeader>` 标签: 打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpHeader>` 标签:
```html ```html
<ng-template #abpHeader> <ng-template #abpHeader>
@ -1206,80 +1055,9 @@ UI最终看起来像这样:
### 删除图书 ### 删除图书
#### DeleteBook 动作
打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件添加名为 `DeleteBook` 的动作.
```js
export class DeleteBook {
static readonly type = '[Book] Delete';
constructor(public id: string) {}
}
```
打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换它:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
book: booksResponse,
});
})
);
}
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
return this.bookService.createByInput(action.payload);
}
}
// <== added DeleteBook action listener ==>
@Action(DeleteBook)
delete(ctx: StateContext<BookStateModel>, action: DeleteBook) {
return this.bookService.deleteById(action.id);
}
}
```
- 我们导入了 `DeleteBook` .
- 我们在文件末尾添加了 `DeleteBook` 动作监听器.
#### 删除确认弹层 #### 删除确认弹层
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`. 打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`.
替换构造函数: 替换构造函数:
@ -1288,50 +1066,46 @@ import { ConfirmationService } from '@abp/ng.theme.shared';
//... //...
constructor( constructor(
private store: Store, public readonly list: ListService,
private fb: FormBuilder, private bookService: BookService,
private bookService: BookService, private fb: FormBuilder,
private confirmation: ConfirmationService // <== added this line ==> private confirmation: ConfirmationService // <== added this line ==>
) { } ) {}
``` ```
* 我们导入了 `ConfirmationService`. * We imported `ConfirmationService`.
* 我们在构造函数注入了 `ConfirmationService` . * We injected `ConfirmationService` to the constructor.
参阅[确认弹层文档](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)了解更多 See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)
在 `book-list.component.ts` 中添加删除方法: In the `book-list.component.ts` add a delete method:
```js ```js
import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==> import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation namespace ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==>
//... //...
delete(id: string) { delete(id: string) {
this.confirmation this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => {
.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') if (status === Confirmation.Status.confirm) {
.subscribe(status => { this.bookService.deleteById(id).subscribe(() => this.list.get());
if (status === Confirmation.Status.confirm) { }
this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get()); });
}
});
} }
``` ```
`delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时分派动作. 确认弹层看起来如下: `delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时调用 `BookService` 的 `deleteById` 方法. 确认弹层看起来如下:
![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) ![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png)
#### 添加删除按钮 #### 添加删除按钮
打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮: 打开 `app\book\book-list` 文件夹下的 `app\book\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮:
```html ```html
<div ngbDropdownMenu> <div ngbDropdownMenu>
<!-- added Delete button --> <!-- added Delete button -->
<button ngbDropdownItem (click)="delete(data.id)"> <button ngbDropdownItem (click)="delete(row.id)">
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%} {%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
</button> </button>
</div> </div>

BIN
docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 25 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-book-list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 56 KiB

89
docs/zh-Hans/UI/Angular/List-Service.md

@ -53,57 +53,38 @@ class BookComponent {
> 注意 `list` 是 `public` 并且 `readonly`. 因为我们将直接在组件的模板中使用 `ListService` 属性. 可以视为反模式,但是实现起来要快得多. 你可以改为使用公共组件属性. > 注意 `list` 是 `public` 并且 `readonly`. 因为我们将直接在组件的模板中使用 `ListService` 属性. 可以视为反模式,但是实现起来要快得多. 你可以改为使用公共组件属性.
将 `ListService` 属性放入模板中,如下所示: 像这样绑定 `ListService` 到 ngx-datatable:
```html ```html
<abp-table <ngx-datatable
[value]="book.items" [rows]="items"
[(page)]="list.page" [count]="count"
[rows]="list.maxResultCount" [list]="list"
[totalRecords]="book.totalCount" default
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[abpLoading]="list.isLoading$ | async"
> >
</abp-table> <!-- column templates here -->
</ngx-datatable>
<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>
``` ```
## 与Observables一起使用 ## 与Observables一起使用
你可以将Observables与Angular的[AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe)结合使用: 你可以将Observables与Angular的[AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe)结合使用:
```ts ```js
book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query)); book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query));
``` ```
```html ```html
<!-- simplified representation of the template --> <!-- simplified representation of the template -->
<abp-table <ngx-datatable
[value]="(book$ | async)?.items || []" [rows]="(book$ | async)?.items || []"
[totalRecords]="(book$ | async)?.totalCount" [count]="(book$ | async)?.totalCount || 0"
[list]="list"
default
> >
</abp-table> <!-- column templates here -->
</ngx-datatable>
<!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE --> <!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE -->
``` ```
@ -111,7 +92,7 @@ class BookComponent {
...or... ...or...
```ts ```js
@Select(BookState.getBooks) @Select(BookState.getBooks)
books$: Observable<BookDto[]>; books$: Observable<BookDto[]>;
@ -126,13 +107,18 @@ class BookComponent {
```html ```html
<!-- simplified representation of the template --> <!-- simplified representation of the template -->
<abp-table <ngx-datatable
[value]="books$ | async" [rows]="(books$ | async) || []"
[totalRecords]="bookCount$ | async" [count]="(bookCount$ | async) || 0"
[list]="list"
default
> >
</abp-table> <!-- column templates here -->
</ngx-datatable>
``` ```
> 我们不建议将NGXS存储用于CRUD页面,除非你的应用程序需要在组件之间共享列表信息或稍后在另一页面中使用它.
## 如何在创建/更新/删除时刷新表 ## 如何在创建/更新/删除时刷新表
`ListService` 公开了一个 `get` 方法来触发当前查询的请求. 因此基本上每当创建,更新或删除操作解析时,你可以调用 `this.list.get();` 它会调用钩子流创建者. `ListService` 公开了一个 `get` 方法来触发当前查询的请求. 因此基本上每当创建,更新或删除操作解析时,你可以调用 `this.list.get();` 它会调用钩子流创建者.
@ -161,3 +147,26 @@ this.bookService.createByInput(form.value)
<input type="text" name="search" [(ngModel)]="list.filter"> <input type="text" name="search" [(ngModel)]="list.filter">
``` ```
## ABP v3.0的重大更改
我们必须修改 `ListService` 使其与 `ngx-datatable` 一起使用. 之前 `page` 属性的最小值为 `1`, 你可以像这样使用它:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[(page)]="list.page"
></abp-table>
```
从v3.0开始, 对于`ngx-datatable`, 初始页面的 `page`属性必须设置为 `0`. 因此如果你以前在表上使用过 `ListService` 并打算保留 `abp-table`,则需要进行以下更改:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[page]="list.page + 1"
(pageChange)="list.page = $event - 1"
></abp-table>
```
**重要提示:** `abp-table` 没有被删除,但是会被弃用,并在将来的版本中移除,请考虑切换到 ngx-datatable.

363
docs/zh-Hans/UI/Angular/Migration-Guide-v3.md

@ -0,0 +1,363 @@
# Angular UI v2.9 迁移到 v3.0 指南
## 在v3.0改变了什么?
### Angular 10
新的ABP Angular UI基于Angular 10和TypeScript 3.9,我们已经放弃了对Angular 8的支持. 不过ABP模块将继续与Angular 9兼容使用. 因此如果你的项目是Angular 9,则无需更新为 Angular10. 更新通常很容易.
#### 如何迁移?
在你的根文件夹中打开一个终端,然后运行以下命令:
```sh
yarn ng update @angular/cli @angular/core --force
```
这会做如下修改:
- 更新你的package.json并安装新的软件包
- 修改tsconfig.json文件创建一个"Solution Style"配置
- 重命名 `browserlist` 为 `.browserlistrc`
另一方面,如果你单独使用 `yarn ng update` 命令检查首先要更新哪些包会更好. Angular会给你一个要更新的包列表.
![Table of packages to update](./images/table-of-packages-to-update.png)
当Angular报告上面的包后,运行命令:
```sh
yarn ng update @angular/cli @angular/core ng-zorro-antd --force
```
> 如果Angular提示你的仓库有中未提交的更改,可以提交/存储它,也可以在命令中添加 `--allow-dirty` 参数.
### 配置模块
在ABP v2.x中,每个延迟加载的模块都有一个可通过单独的程序包使用的配置模块,模块配置如下:
```js
import { AccountConfigModule } from '@abp/ng.account.config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot({ redirectUrl: '/' }),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
...在app-routing.module.ts...
```js
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import(
'./lazy-libs/account-wrapper.module'
).then(m => m.AccountWrapperModule),
},
];
```
虽然有效,但有一些缺点:
- 每个模块都有两个独立的程序包,但实际上这些程序包是相互依赖的.
- 配置延迟加载的模块需要包装模块.
- ABP Commercial具有可扩展性系统,在根模块上配置可扩展模块会增加 bundle 的大小.
在ABP v3.0中,我们为每个配置模块引入了辅助入口点,并且提供了一种在没有包装的情况下配置延迟加载的模块的新方法. 现在模块配置如下所示:
```js
import { AccountConfigModule } from '@abp/ng.account/config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
... 在app-routing.module.ts...
```js
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
];
```
这项更改帮助我们减少了捆绑包的大小并大大缩短了构建时间. 我们相信你会注意到你的应用程序有所不同.
#### 一个更好的例子
AppModule:
```js
import { AccountConfigModule } from '@abp/ng.account/config';
import { CoreModule } from '@abp/ng.core';
import { IdentityConfigModule } from '@abp/ng.identity/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config';
import { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgxsModule } from '@ngxs/store';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
CoreModule.forRoot({
environment,
sendNullsAsQueryParam: false,
skipGetAppConfiguration: false,
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot(),
IdentityConfigModule.forRoot(),
TenantManagementConfigModule.forRoot(),
SettingManagementConfigModule.forRoot(),
ThemeBasicModule.forRoot(),
NgxsModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
AppRoutingModule:
```js
import { DynamicLayoutComponent } from '@abp/ng.core';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./home/home.module')
.then(m => m.HomeModule),
},
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
{
path: 'identity',
loadChildren: () => import('@abp/ng.identity')
.then(m => m.IdentityModule.forLazy()),
},
{
path: 'tenant-management',
loadChildren: () => import('@abp/ng.tenant-management')
.then(m => m.TenantManagementModule.forLazy()),
},
{
path: 'setting-management',
loadChildren: () => import('@abp/ng.setting-management')
.then(m => m.SettingManagementModule.forLazy()),
},
],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
```
> 你可能已经注意到我们在top级别路由组件上使用了 `DynamicLayoutComponent`. 我们这样做是为了避免不必要的渲染和闪烁. 这不是强制的,但是我们建议在你的应用程序路由中做同样的事情.
#### 如何迁移?
- 使用 `yarn remove` 删除你的项目的配置包.
- 从辅助入口点(例如`@abp/ng.identity/config`)导入配置模块.
- 调用所有新配置模块的静态 `forRoot`方法,即使配置没有被传递.
- 调用 `ThemeBasicModule` 的静态 `forRoot` 方法(或商业上的 `ThemeLeptonModule`),并从导入中删除 `SharedModule`(除非已在其中添加了根模块所需的任何内容).
- 在app路由模块中直接导入延迟ABP模块 (如 `() => import('@abp/ng.identity').then(...)`).
- 在所有延迟模块 `then` 中调用的静态 `forLazy` 方法,即使配置没有被传递.
- [可选]使用 `DynamicLayoutComponent` 添加空的父路由,获得更好的性能和UX.
### RoutesService
在ABP v2.x中,通过以下两种方式之一将路由添加到菜单:
- [通过 `AppRoutingModule` 的 `routes` 属性](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule)
- [通过 ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate)
从v3.0开始,我们更改了添加和修改路由的方式. 我们不再将路由存储在 `ConfigState`中(破坏性更改). 而是有一个名为 `RoutesService` 的新服务,该服务用于添加,修补或删除菜单项. 详情请查看[文档](Modifying-the-Menu.md).
#### 如何迁移?
- 检查你是否曾经使用 `ConfigState` 或 `ConfigStateService` 添加路由. 请用 `RoutesService` 的 `add` 方法替换它们.
- 检查你是否曾经修补的路由. 将其替换为 `RoutesService` 的 `patch` 方法.
- 仔细检查你是否使用绝对路径,并在 `add` 或 `patch` 方法调用中为子菜单项提供 `parentName` 而不是 `children` 属性.
### NavItemsService
在ABP v2.x中,[通过LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu)添加导航元素.
从v3.0开始,我们改变了添加和修改导航项的方式,以前的方法不再可用(破坏性更改). 详情请查看[文档](Modifying-the-Menu.md).
#### 如何迁移?
- 用 `NavItemsService` 的 `addItems` 方法替换所有 `dispatchAddNavigationElement` 调用.
### ngx-datatable
在v3之前,我们一直使用自定义组件 `abp-table` 作为默认表. 但是数据表是复杂的组件,要实现功能齐全的数据表需要大量的精力,我们计划将其引入其他功能.
从ABP v3开始,我们已切换到经过严格测试,执行良好的数据表格:[ngx-datatable](https://github.com/swimlane/ngx-datatable). 所有的ABP模块都已经实现了ngx-datatable. `ThemeSharedModule` 已经导出了 `NgxDatatableModule`. 因此如果你在终端运行 `yarn add @swimlane/ngx-datatable` 来安装这个包,它将在你的应用的所有模块中可用.
为了正确设置样式,你需要在angular.json文件的样式部分中添加以下内容:
```json
{
"input": "node_modules/@swimlane/ngx-datatable/index.css",
"inject": true,
"bundleName": "ngx-datatable-index"
},
{
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css",
"inject": true,
"bundleName": "ngx-datatable-icons"
},
{
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css",
"inject": true,
"bundleName": "ngx-datatable-material"
}
```
由于尚未删除 `abp-table`, 因此以前由ABP v2.x构建的模块不会突然丢失所有. 但是它们的外观与内置ABP v3模块有所不同, 因此你可能希望将这些模块中的表转换为ngx-datatable. 为了减少将abp-table转换为ngx-datatable所需的工作量,我们修改了 `ListService` 以使其与 `ngx-datatable` 一起很好地工作,并引入了两个新指令: `NgxDatatableListDirective` 和 `NgxDatatableDefaultDirective`.
这些指令的用法很简单:
```js
@Component({
providers: [ListService],
})
export class SomeComponent {
data$ = this.list.hookToQuery(
query => this.dataService.get(query)
);
constructor(
public readonly list: ListService,
public readonly dataService: SomeDataService,
) {}
}
```
...在组件模板...
```html
<ngx-datatable
[rows]="(data$ | async)?.items || []"
[count]="(data$ | async)?.totalCount || 0"
[list]="list"
default
>
<!-- column templates here -->
</ngx-datatable>
```
通过 `NgxDatatableListDirective` 绑定注入的 `ListService` 实例后,你不再需要担心分页或排序. 同样 `NgxDatatableDefaultDirective` 去除了几个属性绑定,以使ngx-datatable适合我们的样式.
#### 一个更好的例子
```html
<ngx-datatable
[rows]="items"
[count]="count"
[list]="list"
default
>
<!-- the grid actions column -->
<ngx-datatable-column
name=""
[maxWidth]="150"
[width]="150"
[sortable]="false"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="rowIndex"
>
<abp-grid-actions
[index]="i"
[record]="row"
text="AbpUi::Actions"
></abp-grid-actions>
</ng-template>
</ngx-datatable-column>
<!-- a basic column -->
<ngx-datatable-column
prop="someProp"
[name]="'::SomeProp' | abpLocalization"
[width]="200"
></ngx-datatable-column>
<!-- a column with a custom template -->
<ngx-datatable-column
prop="someOtherProp"
[name]="'::SomeOtherProp' | abpLocalization"
[width]="250"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="index"
>
<div abpEllipsis>{%{{{ row.someOtherProp }}}%}</div>
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
```
#### 如何迁移?
- 安装 `@swimlane/ngx-datatable` 包.
- 添加ngx-datatable样式到angular.json文件.
- 如果可以的话,根据上面的例子更新你的模.
- 如果你稍后需要这样做,并且打算保留abp-table一段时间,请确保根据此处描述的[破坏性更改](List-Service.md)更新分页.
**重要说明:**abp-table没有被删除,但已被弃用并在以后的版本中删除. 请考虑切换到ngx-datatable。
### 过时的接口
某些接口早已被标记为已弃用,现在已将其删除.
#### 如何迁移?
请检查你是否仍在使用[Issue中列出的任何内容](https://github.com/abpframework/abp/issues/4281).
## 下一步是什么?
* [服务代理](Service-Proxies.md)

220
docs/zh-Hans/UI/Angular/Modifying-the-Menu.md

@ -1,6 +1,5 @@
# 修改菜单 # 修改菜单
菜单在 @abp/ng.theme.basic包 `ApplicationLayoutComponent` 内部. 有几种修改菜单的方法,本文档介绍了这些方法. 如果你想完全替换菜单,请参考[组件替换文档]了解如何替换布局. 菜单在 @abp/ng.theme.basic包 `ApplicationLayoutComponent` 内部. 有几种修改菜单的方法,本文档介绍了这些方法. 如果你想完全替换菜单,请参考[组件替换文档]了解如何替换布局.
<!-- TODO: Replace layout replacement document with component replacement. Layout replacement document will be created.--> <!-- TODO: Replace layout replacement document with component replacement. Layout replacement document will be created.-->
@ -24,7 +23,97 @@ export const environment = {
## 如何添加导航元素 ## 如何添加导航元素
### 通过 AppRoutingModule 中的 `routes` 属性 ### 通过 `RoutesService`
你可以通过调用 `RoutesService` 的 `add` 方法添加路由到菜单,它是一个单例的服务,在root中提供,你可以立即注入使用它.
```js
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { Component } from '@angular/core';
@Component(/* component metadata */)
export class AppComponent {
constructor(routes: RoutesService) {
routes.add([
{
path: '/your-path',
name: 'Your navigation',
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
layout: eLayoutType.application,
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
order: 1,
requiredPolicy: 'permission key here',
},
]);
}
}
```
另一种方法是使用路由提供程序. 首先创建一个提供程序:
```js
// route.provider.ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
path: '/your-path',
name: 'Your navigation',
requiredPolicy: 'permission key here',
order: 101,
iconClass: 'fas fa-question-circle',
layout: eLayoutType.application,
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
requiredPolicy: 'permission key here',
order: 1,
},
]);
};
}
```
...然后在app.module.ts ...
```js
import { NgModule } from '@angular/core';
import { APP_ROUTE_PROVIDER } from './route.provider';
@NgModule({
providers: [APP_ROUTE_PROVIDER],
// imports, declarations, and bootstrap
})
export class AppModule {}
```
下面是每个属性的工作原理:
- `path` 是导航元素的绝对路径.
- `name` 是导航元素的label. 可以使用本地化Key和本地化对象.
- `parentName` 是菜单中父路由的 `name` 的引用,用于创建多级菜单项.
- `requiredPolicy` 是用于访问该页面的权限Key. 参阅[权限管理文档](Permission-Management.md).
- `order` 是导航元素的排序. `Administration` 的顺序是 `100`. 在排序top级别菜单项时请记得这一点.
- `iconClass` 是 `i` 标签的class, 它放在导航label的左边.
- `layout` 定义路由使用哪个布局加载. (默认: `eLayoutType.empty`).
- `invisible` 使该项在菜单中不可见. (默认: `false`).
### 通过 `AppRoutingModule` 的 `routes` 属性
你可以通过在 `app-routing.module` 中将路由作为子属性添加到路由配置的 `data` 属性来定义路由. `@abp/ng.core` 包组织路由并将其存储在 `ConfigState` 中.`ApplicationLayoutComponent` 从存储中获取路由显示在菜单上. 你可以通过在 `app-routing.module` 中将路由作为子属性添加到路由配置的 `data` 属性来定义路由. `@abp/ng.core` 包组织路由并将其存储在 `ConfigState` 中.`ApplicationLayoutComponent` 从存储中获取路由显示在菜单上.
@ -36,7 +125,7 @@ export const environment = {
data: { data: {
routes: { routes: {
name: 'Your navigation', name: 'Your navigation',
order: 3, order: 101,
iconClass: 'fas fa-question-circle', iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here', requiredPolicy: 'permission key here',
children: [ children: [
@ -47,100 +136,77 @@ export const environment = {
requiredPolicy: 'permission key here', requiredPolicy: 'permission key here',
}, },
], ],
} as ABP.Route, // can be imported from @abp/ng.core },
} },
} }
``` ```
- `name` 是导航元素的标签,可以传递本地化密钥或本地化对象. 或者你可以这样做:
- `order` 排序导航元素.
- `iconClass` 是 `i` 标签的类,在导航标签的左侧.
- `requiredPolicy` 是访问页面所需的权限key. 参阅 [权限管理文档](./Permission-Management.md)
- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property.
- `children` 是一个数组,用于声明子菜单,它基于给定的 `path` 属性,路径是在`/your-path/child`.
添加了上面描述的route属性后,导航菜单如下图所示:
![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## 通过 ConfigState
`ConfigStateService` 的 `dispatchAddRoute` 方法可以向菜单添加一个新的导航元素.
```js
// this.config is instance of ConfigStateService
const newRoute: ABP.Route = {
name: 'My New Page',
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,
order: 2,
requiredPolicy: 'MyProjectName.MyNewPage',
} as Omit<ABP.Route, 'children'>;
this.config.dispatchAddRoute(newRoute);
// returns a state stream which emits after dispatch action is complete
```
`newRoute` 放在根级别,没有任何父路由,url将为`/path`.
如果你想 **添加子路由, 你可以这样做:**
```js ```js
// this.config is instance of ConfigStateService {
// eIdentityRouteNames enum can be imported from @abp/ng.identity path: 'your-path',
data: {
const newRoute: ABP.Route = { routes: [
parentName: eIdentityRouteNames.IdentityManagement, {
name: 'My New Page', path: '/your-path',
iconClass: 'fa fa-dashboard', name: 'Your navigation',
path: 'page', order: 101,
invisible: false, iconClass: 'fas fa-question-circle',
order: 3, requiredPolicy: 'permission key here',
requiredPolicy: 'MyProjectName.MyNewPage' },
} as Omit<ABP.Route, 'children'>; {
path: '/your-path/child',
this.config.dispatchAddRoute(newRoute); name: 'Your child navigation',
// returns a state stream which emits after dispatch action is complete parentName: 'Your navigation',
order: 1,
requiredPolicy: 'permission key here',
},
] as ABP.Route[], // can be imported from @abp/ng.core
},
}
``` ```
`newRoute` 做为 `eIdentityRouteNames.IdentityManagement` 的子路由添加, url 设置为 `'/identity/page'`. 第二种方法的优点是你不必绑定到父/子结构,可以使用任何喜欢的路由.
新路由看起来像这样: 如上所述添加 `routes` 属性后,导航菜单看起来像这样:
![navigation-menu-via-config-state](./images/navigation-menu-via-config-state.png) ![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## 如何修改一个导航元素 ## 如何修补或删除导航元素
`DispatchPatchRouteByName` 方法通过名称查找路由,并使用二个参数传递的新配置替换存储中的配置. `RoutesService` 的 `patch` 方法通过名称查找路由,并将配置替换为第二个参数传递的新配置. `remove` 方法会找到一个路由并将其连同其子路由一起删除.
```js ```js
// this.config is instance of ConfigStateService // this.routes is instance of RoutesService
// eIdentityRouteNames enum can be imported from @abp/ng.identity // eThemeSharedRouteNames enum can be imported from @abp/ng.theme.shared
const dashboardRouteConfig: ABP.Route = {
path: '/dashboard',
name: '::Menu:Dashboard',
parentName: '::Menu:Home',
order: 1,
layout: eLayoutType.application,
};
const newRouteConfig: Partial<ABP.Route> = { const newHomeRouteConfig: Partial<ABP.Route> = {
iconClass: 'fas fa-home', iconClass: 'fas fa-home',
parentName: eIdentityRouteNames.Administration, parentName: eThemeSharedRouteNames.Administration,
order: 0, order: 0,
children: [
{
name: 'Dashboard',
path: 'dashboard',
},
],
}; };
this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig); this.routes.add([dashboardRouteConfig]);
// returns a state stream which emits after dispatch action is complete this.routes.patch('::Menu:Home', newHomeRouteConfig);
this.routes.remove(['Your navigation']);
``` ```
* 根据给定的 `parentName` 将 _Home_ 导航移动到 _Administration_ 下拉框下. - 根据给定的 `parentName` 将 _Home_ 导航移动到 _Administration_ 下拉菜单下.
* 添加了 icon. - 为 _Home_ 添加了图标.
* 指定了顺序. - 指定 _Home_ 的顺序为列表的第一项.
* 添加了名为 _Dashboard_ 的子路由. - 为 _Home_ 添加了一个名为 _Dashboard_ 的子路由.
- 删除 _Your navigation_ 与其子路由.
修改后,导航元素看起来像这样: 上述操作后,新的菜单看起来如下:
![navigation-menu-after-patching](./images/navigation-menu-after-patching.png) ![navigation-menu-after-patching](./images/navigation-menu-after-patching.png)
@ -184,7 +250,7 @@ export class AppComponent {
} }
``` ```
上面我们在菜单添加了一个搜索输入,最终UI如下:s 上面我们在菜单添加了一个搜索输入,最终UI如下:
![navigation-menu-search-input](./images/navigation-menu-search-input.png) ![navigation-menu-search-input](./images/navigation-menu-search-input.png)

BIN
docs/zh-Hans/UI/Angular/images/table-of-packages-to-update.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

278
docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md

@ -1,3 +1,277 @@
## Dynamic Forms # 动态表单
目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/Components/DynamicForms). `提示:` 在开始阅读本文档之前,请确保你已经看过并理解了[abp表单元素](Form-elements.md)文档.
## 介绍
`abp-dynamic-form` 为给定c#模型创建bootstrap表单.
基本用法:
````xml
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/>
````
Model:
````csharp
public class DynamicFormsModel : PageModel
{
[BindProperty]
public DetailedModel MyDetailedModel { get; set; }
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem>
{
new SelectListItem { Value = "CA", Text = "Canada"},
new SelectListItem { Value = "US", Text = "USA"},
new SelectListItem { Value = "UK", Text = "United Kingdom"},
new SelectListItem { Value = "RU", Text = "Russia"}
};
public void OnGet()
{
MyDetailedModel = new DetailedModel
{
Name = "",
Description = "Lorem ipsum dolor sit amet.",
IsActive = true,
Age = 65,
Day = DateTime.Now,
MyCarType = CarType.Coupe,
YourCarType = CarType.Sedan,
Country = "RU",
NeighborCountries = new List<string>() { "UK", "CA" }
};
}
public class DetailedModel
{
[Required]
[Placeholder("Enter your name...")]
[Display(Name = "Name")]
public string Name { get; set; }
[TextArea(Rows = 4)]
[Display(Name = "Description")]
[InputInfoText("Describe Yourself")]
public string Description { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[Display(Name = "Is Active")]
public bool IsActive { get; set; }
[Required]
[Display(Name = "Age")]
public int Age { get; set; }
[Required]
[Display(Name = "My Car Type")]
public CarType MyCarType { get; set; }
[Required]
[AbpRadioButton(Inline = true)]
[Display(Name = "Your Car Type")]
public CarType YourCarType { get; set; }
[DataType(DataType.Date)]
[Display(Name = "Day")]
public DateTime Day { get; set; }
[SelectItems(nameof(CountryList))]
[Display(Name = "Country")]
public string Country { get; set; }
[SelectItems(nameof(CountryList))]
[Display(Name = "Neighbor Countries")]
public List<string> NeighborCountries { get; set; }
}
public enum CarType
{
Sedan,
Hatchback,
StationWagon,
Coupe
}
}
````
## Demo
参阅 [动态表单demo页面](https://bootstrap-taghelpers.abp.io/Components/Dropdowns)查看示例.
## Attributes
### abp-model
为动态表单设置c#模型,模型的属性以表单形式转化为输入.
### submit-button
可以为 `True` 或 `False`.
如果为 `True`,则会在表单底部生成一个提交按钮.
默认值是 `False`.
### required-symbols
可以为 `True` 或 `False`.
如果为 `True`,则必需的输入将带有一个符号(*),表示它们是必需的.
默认值是 `True`.
## 表单内容布局
默认情况下,“`abp-dynamic-form` 会清除内部html并将inputs放入自身. 如果要向动态表单添加其他内容或将inputs放置到某些特定区域,可以使用`<abp-form-content />`标签. 这个标签将被表单内容替换, 而 `abp-dynamic-form` 标签的内部html的其余部分将保持不变.
用法:
````xml
<abp-dynamic-form abp-model="@Model.MyExampleModel">
<div>
Some content....
</div>
<div class="input-area">
<abp-form-content />
</div>
<div>
Some more content....
</div>
</abp-dynamic-form>
````
## 输入排序
`abp-dynamic-form` 通过 `DisplayOrder` attribute对属性进行排序,然后按模型类中的属性顺序进行排序.
默认每个属性的 `DisplayOrder` attribute值是10000.
参见以下示例:
````csharp
public class OrderExampleModel
{
[DisplayOrder(10004)]
public string Name{ get; set; }
[DisplayOrder(10005)]
public string Surname{ get; set; }
//Default 10000
public string EmailAddress { get; set; }
[DisplayOrder(10003)]
public string PhoneNumber { get; set; }
[DisplayOrder(9999)]
public string City { get; set; }
}
````
在这个示例中,inputs字段顺序为: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`.
## 忽略属性
默认情况下, `abp-dynamic-form` 会为模型类中的每个属性生成输入. 如果要忽略属性请使用 `DynamicFormIgnore` attribute.
参见以下示例:
````csharp
public class MyModel
{
public string Name { get; set; }
[DynamicFormIgnore]
public string Surname { get; set; }
}
````
在这个示例中,不会为 `Surname` 属性生成输入.
## 指示文本框,单选按钮组和组合框
如果你已经阅读了[表单元素文档](Form-elements.md),你会注意到在c#模型上 `abp-radio` 和 `abp-select` 标签非常相. 我们必须使用 `[AbpRadioButton()]` attribute来告诉 `abp-dynamic-form` 你的哪些属性是单选按钮组,哪些属性是组合框.
参见以下示例:
````xml
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/>
````
Model:
````csharp
public class DynamicFormsModel : PageModel
{
[BindProperty]
public DetailedModel MyDetailedModel { get; set; }
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem>
{
new SelectListItem { Value = "CA", Text = "Canada"},
new SelectListItem { Value = "US", Text = "USA"},
new SelectListItem { Value = "UK", Text = "United Kingdom"},
new SelectListItem { Value = "RU", Text = "Russia"}
};
public void OnGet()
{
MyDetailedModel = new DetailedModel
{
ComboCarType = CarType.Coupe,
RadioCarType = CarType.Sedan,
ComboCountry = "RU",
RadioCountry = "UK"
};
}
public class DetailedModel
{
public CarType ComboCarType { get; set; }
[AbpRadioButton(Inline = true)]
public CarType RadioCarType { get; set; }
[SelectItems(nameof(CountryList))]
public string ComboCountry { get; set; }
[AbpRadioButton()]
[SelectItems(nameof(CountryList))]
public string RadioCountry { get; set; }
}
public enum CarType
{
Sedan,
Hatchback,
StationWagon,
Coupe
}
}
````
正如你上面的例子中看到:
* 如果在**Enum**属性上使用 `[AbpRadioButton()]`,它将是一个单选按钮组. 否则它是组合框.
* 如果在属性上使用 `[SelectItems()]` 和 `[AbpRadioButton()]`,那么它将是一个单选按钮组.
* 如果只在属性上使用 `[SelectItems()]`,它将是一个组合框.
* 如果一个属性没有使用这些属性,它将是一个文本框.
## 本地化
`abp-dynamic-form` 会处理本地化.
默认情况下, 它将尝试查找 "DisplayName:{PropertyName}" 或 "{PropertyName}" 定位本地化键,并将定位值设置为label.
你可以使用Asp.Net Core的 `[Display()]` attribute自行设置. 可以在此属性中使用本地化密钥. 请参阅以下示例:
````csharp
[Display(Name = "Name")]
public string Name { get; set; }
````

2
docs/zh-Hans/Unit-Of-Work.md

@ -325,7 +325,7 @@ public async Task<int> CreateAsync(string name)
如果你的目的只是在创建/更新/删除实体后保存更改,建议你使用 `autoSave` 选项,而不是手动使用 `CurrentUnitOfWork.SaveChangesAsync()`. 如果你的目的只是在创建/更新/删除实体后保存更改,建议你使用 `autoSave` 选项,而不是手动使用 `CurrentUnitOfWork.SaveChangesAsync()`.
> **Note-1**: 当工作单元结束而没有任何错误时,所有更改都会自动保存. 所以除非确实需要,否则不要调用 `SaveChangesAsync()`. > **Note-1**: 当工作单元结束而没有任何错误时,所有更改都会自动保存. 所以除非确实需要,否则不要调用 `SaveChangesAsync()` 和设置 `autoSave` 为 `true`.
> >
> **Note-2**: 如果你使用 `Guid` 作为主键,则无需插入时保存来获取生成的id,因为 `Guid` 主键是在应用程序中设置的,创建新实体后立即可用. > **Note-2**: 如果你使用 `Guid` 作为主键,则无需插入时保存来获取生成的id,因为 `Guid` 主键是在应用程序中设置的,创建新实体后立即可用.

172
docs/zh-Hans/Virtual-File-System.md

@ -2,109 +2,77 @@
虚拟文件系统使得管理物理上不存在于文件系统中(磁盘)的文件成为可能. 它主要用于将(js, css, image...)文件嵌入到程序集中, 并在运行时将它们象物理文件一样使用. 虚拟文件系统使得管理物理上不存在于文件系统中(磁盘)的文件成为可能. 它主要用于将(js, css, image...)文件嵌入到程序集中, 并在运行时将它们象物理文件一样使用.
### Volo.Abp.VirtualFileSystem nuget包 ## 安装
Volo.Abp.VirtualFileSystem是虚拟文件系统的核心包. 使用程序包管理器控制台(PMC)将其安装到项目中: > 大多数情况下你不需要手动安装这个包,因为[应用程序启动模板](Startup-Templates/Application.md)已经预先安装.
``` [Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) 是虚拟文件系统的NuGet主页.
Install-Package Volo.Abp.VirtualFileSystem
```
> 启动模板默认已经安装了此nuget包, 所以在大多数情况下你不需要手动安装它. 使用ABP CLIi添加包到你的项目:
然后你可以在module中添加 **AbpVirtualFileSystemModule** 依赖项: * 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装.
* 在你想要添加 `Volo.Abp.VirtualFileSystem` 包的项目的 `.csproj` 文件目录打开命令行(终端).
* 运行 `abp add-package Volo.Abp.VirtualFileSystem` 命令.
```c# 如果你想要手动安装,安装[Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem)NuGet包到你的项目并且添加`[DependsOn(typeof(AbpVirtualFileSystemModule))]`到你项目的[ABP Module](Module-Development-Basics.md)类.
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace MyCompany.MyProject ## 与嵌入式文件工作
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
//...
}
}
```
#### 注册嵌入式文件 ### 嵌入文件
要将文件嵌入到程序集中, 首先需要把该文件标记为嵌入式资源. 最简单的方式是在 **解决方案管理器** 中选择文件, 然后找到 **"属性"** 窗口将 **"生成操作"** 设置为 **"嵌入式资源"**. 要将文件嵌入到程序集中, 首先需要把该文件标记为**嵌入式资源**. 最简单的方式是在 **解决方案管理器** 中选择文件, 然后找到 **"属性"** 窗口将 **"生成操作"** 设置为 **"嵌入式资源"**.
例如: 例如:
![build-action-embedded-resource-sample](images/build-action-embedded-resource-sample.png) ![build-action-embedded-resource-sample](images/build-action-embedded-resource-sample.png)
如果需要添加多个文件, 这样做会很乏味. 作为选择, 你可以直接编辑 **.csproj** 文件: 如果需要添加多个文件, 这样做会很乏味. 作为选择, 你可以直接编辑 **.csproj** 文件:
````C# 如果文件名包含一些特殊字符,在项目/程序集中嵌入文件可能会导致问题. 为了克服这个限制;
<ItemGroup>
<EmbeddedResource Include="MyResources\**\*.*" /> 1. 将[Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet包添加到包含嵌入式资源的项目中.
<Content Remove="MyResources\**\*.*" /> 2. 添加 `<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>` 到 `.csproj` 文件的 `<PropertyConfig>...</PropertyConfig>` 部分中.
</ItemGroup>
````
此配置以递归方式添加项目的 **MyResources** 文件夹下的所有文件(包括将来新添加的文件). 此配置以递归方式添加项目的 **MyResources** 文件夹下的所有文件(包括将来新添加的文件).
然后需要使用 `AbpVirtualFileSystemOptions` 来配置模块, 以便将嵌入式文件注册到虚拟文件系统. 例如: > 尽管这两个步骤是可选的,并且ABP无需这些配置即可工作,但强烈建议你这样做.
````C# ### 配置AbpVirtualFileSystemOptions
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace MyCompany.MyProject 使用 `AbpVirtualFileSystemOptions` [选项类](Options.md)可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中将嵌入式文件注册到虚拟文件系统.
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
//Register all embedded files of this assembly to the virtual file system
options.FileSets.AddEmbedded<MyModule>("YourRootNameSpace");
});
//... **示例: 添加嵌入式文件到虚拟文件系统**
}
}
}
````
`AddEmbedded` 扩展方法需要一个类, 从给定类的程序集中查找所有嵌入文件, 并将它们注册到虚拟文件系统. 它还有更简洁的写法:
````C# ````csharp
options.FileSets.Add(new EmbeddedFileSet(typeof(MyModule).Assembly), "YourRootNameSpace"); Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyModule>();
});
```` ````
> "YourRootNameSpace" 是项目的根命名空间名字. 如果你的项目的根命名空间名字为空,则无需传递此参数. `AddEmbedded` 扩展方法需要一个类, 从给定**类的程序集中查找所有嵌入文件**, 并将它们注册到虚拟文件系统. 通常将模块类作为通用参数传递.
#### 获取虚拟文件: IVirtualFileProvider `AddEmbedded` 有两个可选参数;
将文件嵌入到程序集中并注册到虚拟文件系统后, 可以使用`IVirtualFileProvider`来获取文件或目录内容: * `baseNamespace`: 仅在你没有配置上面说明的 `GenerateEmbeddedFilesManifest` 并且你的根名称空间不为空时,才可能需要这样做. 在这种情况下,请在此处设置你的根名称空间.
* `baseFolder`: 如果你不想公开项目中的所有嵌入式文件,只希望公开特定的文件夹(和子文件夹/文件),可以相对于项目根页面设置基本文件夹.
````C# **示例: 添加项目中 `MyFiles` 目录下的文件**
public class MyService
{
private readonly IVirtualFileProvider _virtualFileProvider;
public MyService(IVirtualFileProvider virtualFileProvider) ````csharp
{ Configure<AbpVirtualFileSystemOptions>(options =>
_virtualFileProvider = virtualFileProvider; {
} options.FileSets.AddEmbedded<MyModule>(
baseNamespace: "Acme.BookStore.MyFiles",
baseFolder: "/MyFiles"
);
});
````
public void Foo() 这个例子假设;
{
//Getting a single file
var file = _virtualFileProvider.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString(); //ReadAsString is an extension method of ABP
//Getting all files/directories under a directory * 你的项目根(default)命令空间是 `Acme.BookStore`.
var directoryContents = _virtualFileProvider.GetDirectoryContents("/MyResources/js"); * 你的项目有一个名为 `MyFiles` 的目录.
} * 你只想添加 `MyFiles` 目录到虚拟文件系统.
}
````
#### 在开发过程中处理嵌入式文件 #### 在开发过程中处理嵌入式文件
@ -128,27 +96,61 @@ public class MyWebAppModule : AbpModule
{ {
Configure<AbpVirtualFileSystemOptions>(options => Configure<AbpVirtualFileSystemOptions>(options =>
{ {
//ReplaceEmbeddedByPhysical gets the root folder of the MyModule project
options.FileSets.ReplaceEmbeddedByPhysical<MyModule>( options.FileSets.ReplaceEmbeddedByPhysical<MyModule>(
Path.Combine(hostingEnvironment.ContentRootPath, "..\\MyModuleProject") Path.Combine(
hostingEnvironment.ContentRootPath,
string.Format(
"..{0}MyModuleProject",
Path.DirectorySeparatorChar
)
)
); );
}); });
} }
//...
} }
} }
```` ````
上面的代码假设`MyWebAppModule`和`MyModule`是Visual Studio解决方案中的两个不同的项目, `MyWebAppModule`依赖于`MyModule`. 上面的代码假设`MyWebAppModule`和`MyModule`是Visual Studio解决方案中的两个不同的项目, `MyWebAppModule`依赖于`MyModule`.
> [应用程序启动模板]已经为本地化文件应用这个方法,所以当你更改一个本地化文件时,它会自动检测到更改.
## IVirtualFileProvider
将文件嵌入到程序集中并注册到虚拟文件系统后,可以使用 `IVirtualFileProvider` 接口来获取文件或目录内容:
````C#
public class MyService
{
private readonly IVirtualFileProvider _virtualFileProvider;
public MyService(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
}
public void Foo()
{
//Getting a single file
var file = _virtualFileProvider
.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString();
//Getting all files/directories under a directory
var directoryContents = _virtualFileProvider
.GetDirectoryContents("/MyResources/js");
}
}
````
### ASP.NET Core 集成 ### ASP.NET Core 集成
虚拟文件系统与 ASP.NET Core 无缝集成: 虚拟文件系统与 ASP.NET Core 无缝集成:
* 虚拟文件可以像Web应用程序上的物理(静态)文件一样使用. * 虚拟文件可以像Web应用程序上的物理(静态)文件一样使用.
* Js, css, 图像文件和所有其他Web内容可以嵌入到程序集中并像物理文件一样使用. * Js, css, 图像文件和所有其他Web内容可以嵌入到程序集中并像物理文件一样使用.
* 应用程序(或其他模块)可以覆盖模块的虚拟文件, 就像将具有相同名称和扩展名的文件放入虚拟文件的同一文件夹中一样. * 应用程序(或其他模块)可以**覆盖模块的虚拟文件**, 就像将具有相同名称和扩展名的文件放入虚拟文件的同一文件夹中一样.
#### 虚拟文件中间件 #### 虚拟文件中间件
@ -160,4 +162,14 @@ app.UseVirtualFiles();
在静态文件中间件之后添加虚拟文件中间件, 使得通过在虚拟文件相同的位置放置物理文件, 从而用物理文件覆盖虚拟文件成为可能. 在静态文件中间件之后添加虚拟文件中间件, 使得通过在虚拟文件相同的位置放置物理文件, 从而用物理文件覆盖虚拟文件成为可能.
> 虚拟文件中间件可以虚拟wwwroot文件夹中的内容 - 就像静态文件一样. > [应用程序启动模板](Startup-Templates/Application.md)已经配置了 `UseVirtualFiles()`.
#### 静态虚拟文件夹
默认情况下,ASP.NET Core仅允许 `wwwroot` 文件夹包含客户端使用的静态文件. 当你使用 `UseVirtualFiles` 中间件时以下文件夹也可以包含静态文件:
* Pages
* Views
* Themes
这允许你可以在 `.cshtml` 文件附近添加 `.js`, `.css`... 文件,更易于开发和维护你的项目.

4
docs/zh-Hans/docs-nav.json

@ -368,6 +368,10 @@
{ {
"text": "Angular", "text": "Angular",
"items": [ "items": [
{
"text": "v2.x 到 v3 迁移指南",
"path": "UI/Angular/Migration-Guide-v3.md"
},
{ {
"text": "服务代理", "text": "服务代理",
"path": "UI/Angular/Service-Proxies.md" "path": "UI/Angular/Service-Proxies.md"

14
framework/Volo.Abp.sln

@ -307,6 +307,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Azure.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.Oracle", "src\Volo.Abp.EntityFrameworkCore.Oracle\Volo.Abp.EntityFrameworkCore.Oracle.csproj", "{1738845A-5348-4EB8-B736-CD1D22A808B4}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.Oracle", "src\Volo.Abp.EntityFrameworkCore.Oracle\Volo.Abp.EntityFrameworkCore.Oracle.csproj", "{1738845A-5348-4EB8-B736-CD1D22A808B4}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Caching.StackExchangeRedis", "src\Volo.Abp.Caching.StackExchangeRedis\Volo.Abp.Caching.StackExchangeRedis.csproj", "{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Caching.StackExchangeRedis.Tests", "test\Volo.Abp.Caching.StackExchangeRedis.Tests\Volo.Abp.Caching.StackExchangeRedis.Tests.csproj", "{60D0E384-965E-4F81-9D71-B28F419254FC}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -913,6 +917,14 @@ Global
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {1738845A-5348-4EB8-B736-CD1D22A808B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {1738845A-5348-4EB8-B736-CD1D22A808B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Release|Any CPU.Build.0 = Release|Any CPU {1738845A-5348-4EB8-B736-CD1D22A808B4}.Release|Any CPU.Build.0 = Release|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Release|Any CPU.Build.0 = Release|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -1068,6 +1080,8 @@ Global
{C44242F7-D55D-4867-AAF4-A786E404312E} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {C44242F7-D55D-4867-AAF4-A786E404312E} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{A80E9A0B-8932-4B5D-83FB-6751708FD484} = {447C8A77-E5F0-4538-8687-7383196D04EA} {A80E9A0B-8932-4B5D-83FB-6751708FD484} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{1738845A-5348-4EB8-B736-CD1D22A808B4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {1738845A-5348-4EB8-B736-CD1D22A808B4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{60D0E384-965E-4F81-9D71-B28F419254FC} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

27
framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53450/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Authentication.OAuth": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53451/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.MultiTenancy/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53760/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.MultiTenancy": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53763/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.Client/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:52302/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.Client": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:52303/"
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs

@ -14,5 +14,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
public string UserName { get; set; } public string UserName { get; set; }
public string Email { get; set; } public string Email { get; set; }
public string[] Roles { get; set; }
} }
} }

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61183/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Bootstrap": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53762/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Bundling": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53765/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:56918/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.TenantSwitch": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:56919/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53766/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Packages": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53768/"
}
}
}

13
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Uppy/UppyScriptContributor.cs

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Uppy
{
public class UppyScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/uppy/uppy.min.js");
}
}
}

13
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Uppy/UppyStyleContributor.cs

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Uppy
{
public class UppyStyleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/uppy/uppy.min.css");
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50398/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:50399/"
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj

@ -22,8 +22,6 @@
<ItemGroup> <ItemGroup>
<Content Remove="wwwroot\**\*.*" /> <Content Remove="wwwroot\**\*.*" />
<Content Remove="Properties\launchSettings.json" />
<None Include="Properties\launchSettings.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj

@ -18,8 +18,6 @@
<ItemGroup> <ItemGroup>
<Content Remove="compilerconfig.json" /> <Content Remove="compilerconfig.json" />
<Content Remove="Properties\launchSettings.json" />
<None Include="Properties\launchSettings.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

3
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/AbpPageSearchBoxViewComponent.cs

@ -1,5 +1,4 @@
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageSearchBox namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageSearchBox
{ {

10
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/Default.cshtml

@ -1,13 +1,17 @@
@using Localization.Resources.AbpUi @using Localization.Resources.AbpUi
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@inject IStringLocalizer<AbpUiResource> L @inject IStringLocalizer<AbpUiResource> L
<form method="post" class="page-search-form"> <form method="post" class="page-search-form">
<div class="input-group"> <div class="input-group">
<input class="form-control page-search-filter-text" placeholder="@L["Search"].Value"> <input class="form-control page-search-filter-text" placeholder="@L["Search"].Value" />
<div class="input-group-append"> <div class="input-group-append">
<abp-button button-type="Primary" type="submit"> <abp-button button-type="Primary" type="submit">
<i class="fa fa-search" aria-hidden="true" style="margin-left: 10%; margin-top:50%;" ></i> <i class="fa fa-search" aria-hidden="true"></i>
</abp-button> </abp-button>
</div> </div>
</div> </div>
</form> </form>

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/_ViewImports.cshtml

@ -0,0 +1,4 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:51544/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:51545/"
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj

@ -23,9 +23,7 @@
<ItemGroup> <ItemGroup>
<Content Remove="wwwroot\**\*.*" /> <Content Remove="wwwroot\**\*.*" />
<Content Remove="compilerconfig.json" /> <Content Remove="compilerconfig.json" />
<Content Remove="Properties\launchSettings.json" />
<None Include="compilerconfig.json" /> <None Include="compilerconfig.json" />
<None Include="Properties\launchSettings.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53761/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Widgets": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53764/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55913/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53767/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53769/"
}
}
}

5
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs

@ -16,6 +16,7 @@ using System.Net;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
@ -25,6 +26,7 @@ using Microsoft.Extensions.Localization;
using Volo.Abp.ApiVersioning; using Volo.Abp.ApiVersioning;
using Volo.Abp.AspNetCore.Mvc.ApiExploring; using Volo.Abp.AspNetCore.Mvc.ApiExploring;
using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.DataAnnotations;
using Volo.Abp.AspNetCore.Mvc.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.Json; using Volo.Abp.AspNetCore.Mvc.Json;
using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.Localization;
@ -164,6 +166,9 @@ namespace Volo.Abp.AspNetCore.Mvc
partManager.FeatureProviders.Add(new AbpConventionalControllerFeatureProvider(application)); partManager.FeatureProviders.Add(new AbpConventionalControllerFeatureProvider(application));
partManager.ApplicationParts.AddIfNotContains(typeof(AbpAspNetCoreMvcModule).Assembly); partManager.ApplicationParts.AddIfNotContains(typeof(AbpAspNetCoreMvcModule).Assembly);
context.Services.Replace(ServiceDescriptor.Singleton<IValidationAttributeAdapterProvider, AbpValidationAttributeAdapterProvider>());
context.Services.AddSingleton<ValidationAttributeAdapterProvider>();
Configure<MvcOptions>(mvcOptions => Configure<MvcOptions>(mvcOptions =>
{ {
mvcOptions.AddAbp(context.Services); mvcOptions.AddAbp(context.Services);

1
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs

@ -5,7 +5,6 @@ using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
using Volo.Abp.AspNetCore.Mvc.Features; using Volo.Abp.AspNetCore.Mvc.Features;
using Volo.Abp.AspNetCore.Mvc.ModelBinding; using Volo.Abp.AspNetCore.Mvc.ModelBinding;
using Volo.Abp.AspNetCore.Mvc.ModelBinding.Metadata;
using Volo.Abp.AspNetCore.Mvc.Response; using Volo.Abp.AspNetCore.Mvc.Response;
using Volo.Abp.AspNetCore.Mvc.Uow; using Volo.Abp.AspNetCore.Mvc.Uow;
using Volo.Abp.AspNetCore.Mvc.Validation; using Volo.Abp.AspNetCore.Mvc.Validation;

3
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs

@ -117,7 +117,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
Id = _currentUser.Id, Id = _currentUser.Id,
TenantId = _currentUser.TenantId, TenantId = _currentUser.TenantId,
UserName = _currentUser.UserName, UserName = _currentUser.UserName,
Email = _currentUser.Email Email = _currentUser.Email,
Roles = _currentUser.Roles
}; };
} }

34
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/AbpValidationAttributeAdapterProvider.cs

@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations
{
public class AbpValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
private readonly ValidationAttributeAdapterProvider _defaultAdapter;
public AbpValidationAttributeAdapterProvider(ValidationAttributeAdapterProvider defaultAdapter)
{
_defaultAdapter = defaultAdapter;
}
public virtual IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
{
var type = attribute.GetType();
if (type == typeof(DynamicStringLengthAttribute))
{
return new DynamicStringLengthAttributeAdapter((DynamicStringLengthAttribute) attribute, stringLocalizer);
}
if (type == typeof(DynamicMaxLengthAttribute))
{
return new DynamicMaxLengthAttributeAdapter((DynamicMaxLengthAttribute) attribute, stringLocalizer);
}
return _defaultAdapter.GetAttributeAdapter(attribute, stringLocalizer);
}
}
}

46
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicMaxLengthAttributeAdapter.cs

@ -0,0 +1,46 @@
using System.Globalization;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations
{
public class DynamicMaxLengthAttributeAdapter : AttributeAdapterBase<DynamicMaxLengthAttribute>
{
private readonly string _max;
public DynamicMaxLengthAttributeAdapter(
DynamicMaxLengthAttribute attribute,
IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
_max = Attribute.Length.ToString(CultureInfo.InvariantCulture);
}
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
Check.NotNull(validationContext, nameof(validationContext));
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.Length
);
}
public override void AddValidation(ClientModelValidationContext context)
{
Check.NotNull(context, nameof(context));
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-maxlength", GetErrorMessage(context));
MergeAttribute(context.Attributes, "data-val-maxlength-max", _max);
if (Attribute.Length != int.MaxValue)
{
MergeAttribute(context.Attributes, "data-val-maxlength-max", _max);
}
}
}
}

54
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicStringLengthAttributeAdapter.cs

@ -0,0 +1,54 @@
using System;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations
{
public class DynamicStringLengthAttributeAdapter : AttributeAdapterBase<DynamicStringLengthAttribute>
{
private readonly string _max;
private readonly string _min;
public DynamicStringLengthAttributeAdapter(
DynamicStringLengthAttribute attribute,
IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
_max = Attribute.MaximumLength.ToString(CultureInfo.InvariantCulture);
_min = Attribute.MinimumLength.ToString(CultureInfo.InvariantCulture);
}
public override void AddValidation(ClientModelValidationContext context)
{
Check.NotNull(context, nameof(context));
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-length", GetErrorMessage(context));
if (Attribute.MaximumLength != int.MaxValue)
{
MergeAttribute(context.Attributes, "data-val-length-max", _max);
}
if (Attribute.MinimumLength != 0)
{
MergeAttribute(context.Attributes, "data-val-length-min", _min);
}
}
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
Check.NotNull(validationContext, nameof(validationContext));
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.MaximumLength,
Attribute.MinimumLength
);
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Serilog/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61851/",
"sslPort": 44301
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Serilog": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53900/",
"sslPort": 44362
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.SignalR": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.TestBase/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53783/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.TestBase": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53784/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53374/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53375/"
}
}
}

6
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpBackgroundWorkersQuartzModule.cs

@ -1,6 +1,7 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
using Volo.Abp.Quartz; using Volo.Abp.Quartz;
@ -18,6 +19,11 @@ namespace Volo.Abp.BackgroundWorkers.Quartz
context.Services.AddConventionalRegistrar(new AbpQuartzConventionalRegistrar()); context.Services.AddConventionalRegistrar(new AbpQuartzConventionalRegistrar());
} }
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton(typeof(QuartzPeriodicBackgroundWorkerAdapter<>));
}
public override void OnPreApplicationInitialization(ApplicationInitializationContext context) public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
{ {
var options = context.ServiceProvider.GetService<IOptions<AbpBackgroundWorkerOptions>>().Value; var options = context.ServiceProvider.GetService<IOptions<AbpBackgroundWorkerOptions>>().Value;

7
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/IQuartzBackgroundWorkerAdapter.cs

@ -0,0 +1,7 @@
namespace Volo.Abp.BackgroundWorkers.Quartz
{
public interface IQuartzBackgroundWorkerAdapter : IQuartzBackgroundWorker
{
void BuildWorker(IBackgroundWorker worker);
}
}

88
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerAdapter.cs

@ -0,0 +1,88 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Quartz;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers.Quartz
{
public class QuartzPeriodicBackgroundWorkerAdapter<TWorker> : QuartzBackgroundWorkerBase,
IQuartzBackgroundWorkerAdapter
where TWorker : IBackgroundWorker
{
private readonly MethodInfo _doWorkAsyncMethod;
private readonly MethodInfo _doWorkMethod;
public QuartzPeriodicBackgroundWorkerAdapter()
{
AutoRegister = false;
_doWorkAsyncMethod = typeof(TWorker).GetMethod("DoWorkAsync", BindingFlags.Instance | BindingFlags.NonPublic);
_doWorkMethod = typeof(TWorker).GetMethod("DoWork", BindingFlags.Instance | BindingFlags.NonPublic);
}
public void BuildWorker(IBackgroundWorker worker)
{
int? period;
var workerType = worker.GetType();
if (worker is AsyncPeriodicBackgroundWorkerBase || worker is PeriodicBackgroundWorkerBase)
{
if (typeof(TWorker) != worker.GetType())
{
throw new ArgumentException($"{nameof(worker)} type is different from the generic type");
}
var timer = (AbpTimer) worker.GetType().GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker);
period = timer?.Period;
}
else
{
return;
}
if (period == null)
{
return;
}
JobDetail = JobBuilder
.Create<QuartzPeriodicBackgroundWorkerAdapter<TWorker>>()
.WithIdentity(workerType.FullName)
.Build();
Trigger = TriggerBuilder.Create()
.WithIdentity(workerType.FullName)
.WithSimpleSchedule(builder => builder.WithInterval(TimeSpan.FromMilliseconds(period.Value)).RepeatForever())
.Build();
}
public override async Task Execute(IJobExecutionContext context)
{
var worker = (IBackgroundWorker) ServiceProvider.GetService(typeof(TWorker));
var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider);
switch (worker)
{
case AsyncPeriodicBackgroundWorkerBase asyncWorker:
{
if (_doWorkAsyncMethod != null)
{
await (Task) _doWorkAsyncMethod.Invoke(asyncWorker, new object[] {workerContext});
}
break;
}
case PeriodicBackgroundWorkerBase syncWorker:
{
if (_doWorkMethod != null)
{
_doWorkMethod.Invoke(syncWorker, new object[] {workerContext});
}
break;
}
}
}
}
}

18
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs

@ -1,4 +1,5 @@
using System.Threading; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Quartz; using Quartz;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
@ -50,6 +51,19 @@ namespace Volo.Abp.BackgroundWorkers.Quartz
await DefaultScheduleJobAsync(quartzWork); await DefaultScheduleJobAsync(quartzWork);
} }
} }
else
{
var adapterType = typeof(QuartzPeriodicBackgroundWorkerAdapter<>).MakeGenericType(worker.GetType());
var workerAdapter = Activator.CreateInstance(adapterType) as IQuartzBackgroundWorkerAdapter;
workerAdapter?.BuildWorker(worker);
if (workerAdapter?.Trigger != null)
{
await DefaultScheduleJobAsync(workerAdapter);
}
}
} }
protected virtual async Task DefaultScheduleJobAsync(IQuartzBackgroundWorker quartzWork) protected virtual async Task DefaultScheduleJobAsync(IQuartzBackgroundWorker quartzWork)
@ -66,4 +80,4 @@ namespace Volo.Abp.BackgroundWorkers.Quartz
} }
} }
} }
} }

1
framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs

@ -15,6 +15,7 @@ namespace Volo.Abp.BlobStoring.Azure
Action<AzureBlobProviderConfiguration> azureConfigureAction) Action<AzureBlobProviderConfiguration> azureConfigureAction)
{ {
containerConfiguration.ProviderType = typeof(AzureBlobProvider); containerConfiguration.ProviderType = typeof(AzureBlobProvider);
containerConfiguration.NamingNormalizers.TryAdd<AzureBlobNamingNormalizer>();
azureConfigureAction(new AzureBlobProviderConfiguration(containerConfiguration)); azureConfigureAction(new AzureBlobProviderConfiguration(containerConfiguration));

52
framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobNamingNormalizer.cs

@ -0,0 +1,52 @@
using System.Text.RegularExpressions;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BlobStoring.Azure
{
public class AzureBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency
{
/// <summary>
///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names
/// </summary>
public virtual string NormalizeContainerName(string containerName)
{
// All letters in a container name must be lowercase.
containerName = containerName.ToLower();
// Container names can contain only letters, numbers, and the dash (-) character.
containerName = Regex.Replace(containerName, "[^a-z0-9-]", string.Empty);
// Every dash (-) character must be immediately preceded and followed by a letter or number;
// consecutive dashes are not permitted in container names.
// Container names must start or end with a letter or number
containerName = Regex.Replace(containerName, "-{2,}", "-");
containerName = Regex.Replace(containerName, "^-", string.Empty);
containerName = Regex.Replace(containerName, "-$", string.Empty);
// Container names must be from 3 through 63 characters long.
if (containerName.Length < 3)
{
var length = containerName.Length;
for (var i = 0; i < 3 - length; i++)
{
containerName += "0";
}
}
if (containerName.Length > 63)
{
containerName = containerName.Substring(0, 63);
}
return containerName;
}
/// <summary>
///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#blob-names
/// </summary>
public virtual string NormalizeBlobName(string blobName)
{
return blobName;
}
}
}

9
framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs

@ -9,16 +9,17 @@ namespace Volo.Abp.BlobStoring.FileSystem
{ {
return new FileSystemBlobProviderConfiguration(containerConfiguration); return new FileSystemBlobProviderConfiguration(containerConfiguration);
} }
public static BlobContainerConfiguration UseFileSystem( public static BlobContainerConfiguration UseFileSystem(
this BlobContainerConfiguration containerConfiguration, this BlobContainerConfiguration containerConfiguration,
Action<FileSystemBlobProviderConfiguration> fileSystemConfigureAction) Action<FileSystemBlobProviderConfiguration> fileSystemConfigureAction)
{ {
containerConfiguration.ProviderType = typeof(FileSystemBlobProvider); containerConfiguration.ProviderType = typeof(FileSystemBlobProvider);
containerConfiguration.NamingNormalizers.TryAdd<FileSystemBlobNamingNormalizer>();
fileSystemConfigureAction(new FileSystemBlobProviderConfiguration(containerConfiguration)); fileSystemConfigureAction(new FileSystemBlobProviderConfiguration(containerConfiguration));
return containerConfiguration; return containerConfiguration;
} }
} }
} }

41
framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobNamingNormalizer.cs

@ -0,0 +1,41 @@
using System;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BlobStoring.FileSystem
{
public class FileSystemBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency
{
private readonly IOSPlatformProvider _iosPlatformProvider;
public FileSystemBlobNamingNormalizer(IOSPlatformProvider iosPlatformProvider)
{
_iosPlatformProvider = iosPlatformProvider;
}
public virtual string NormalizeContainerName(string containerName)
{
return Normalize(containerName);
}
public virtual string NormalizeBlobName(string blobName)
{
return Normalize(blobName);
}
protected virtual string Normalize(string fileName)
{
var os = _iosPlatformProvider.GetCurrentOSPlatform();
if (os == OSPlatform.Windows)
{
// A filename cannot contain any of the following characters: \ / : * ? " < > |
// In order to support the directory included in the blob name, remove / and \
fileName = Regex.Replace(fileName, "[:\\*\\?\"<>\\|]", string.Empty);
}
return fileName;
}
}
}

62
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs

@ -1,7 +1,9 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading; using Volo.Abp.Threading;
@ -84,18 +86,22 @@ namespace Volo.Abp.BlobStoring
protected ICancellationTokenProvider CancellationTokenProvider { get; } protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IServiceProvider ServiceProvider { get; }
public BlobContainer( public BlobContainer(
string containerName, string containerName,
BlobContainerConfiguration configuration, BlobContainerConfiguration configuration,
IBlobProvider provider, IBlobProvider provider,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
ICancellationTokenProvider cancellationTokenProvider) ICancellationTokenProvider cancellationTokenProvider,
IServiceProvider serviceProvider)
{ {
ContainerName = containerName; ContainerName = containerName;
Configuration = configuration; Configuration = configuration;
Provider = provider; Provider = provider;
CurrentTenant = currentTenant; CurrentTenant = currentTenant;
CancellationTokenProvider = cancellationTokenProvider; CancellationTokenProvider = cancellationTokenProvider;
ServiceProvider = serviceProvider;
} }
public virtual async Task SaveAsync( public virtual async Task SaveAsync(
@ -106,11 +112,13 @@ namespace Volo.Abp.BlobStoring
{ {
using (CurrentTenant.Change(GetTenantIdOrNull())) using (CurrentTenant.Change(GetTenantIdOrNull()))
{ {
var (normalizedContainerName, normalizedBlobName) = NormalizeNaming(ContainerName, name);
await Provider.SaveAsync( await Provider.SaveAsync(
new BlobProviderSaveArgs( new BlobProviderSaveArgs(
ContainerName, normalizedContainerName,
Configuration, Configuration,
name, normalizedBlobName,
stream, stream,
overrideExisting, overrideExisting,
CancellationTokenProvider.FallbackToProvider(cancellationToken) CancellationTokenProvider.FallbackToProvider(cancellationToken)
@ -125,11 +133,14 @@ namespace Volo.Abp.BlobStoring
{ {
using (CurrentTenant.Change(GetTenantIdOrNull())) using (CurrentTenant.Change(GetTenantIdOrNull()))
{ {
var (normalizedContainerName, normalizedBlobName) =
NormalizeNaming(ContainerName, name);
return await Provider.DeleteAsync( return await Provider.DeleteAsync(
new BlobProviderDeleteArgs( new BlobProviderDeleteArgs(
ContainerName, normalizedContainerName,
Configuration, Configuration,
name, normalizedBlobName,
CancellationTokenProvider.FallbackToProvider(cancellationToken) CancellationTokenProvider.FallbackToProvider(cancellationToken)
) )
); );
@ -142,11 +153,14 @@ namespace Volo.Abp.BlobStoring
{ {
using (CurrentTenant.Change(GetTenantIdOrNull())) using (CurrentTenant.Change(GetTenantIdOrNull()))
{ {
var (normalizedContainerName, normalizedBlobName) =
NormalizeNaming(ContainerName, name);
return await Provider.ExistsAsync( return await Provider.ExistsAsync(
new BlobProviderExistsArgs( new BlobProviderExistsArgs(
ContainerName, normalizedContainerName,
Configuration, Configuration,
name, normalizedBlobName,
CancellationTokenProvider.FallbackToProvider(cancellationToken) CancellationTokenProvider.FallbackToProvider(cancellationToken)
) )
); );
@ -158,7 +172,7 @@ namespace Volo.Abp.BlobStoring
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var stream = await GetOrNullAsync(name, cancellationToken); var stream = await GetOrNullAsync(name, cancellationToken);
if (stream == null) if (stream == null)
{ {
//TODO: Consider to throw some type of "not found" exception and handle on the HTTP status side //TODO: Consider to throw some type of "not found" exception and handle on the HTTP status side
@ -175,11 +189,14 @@ namespace Volo.Abp.BlobStoring
{ {
using (CurrentTenant.Change(GetTenantIdOrNull())) using (CurrentTenant.Change(GetTenantIdOrNull()))
{ {
var (normalizedContainerName, normalizedBlobName) =
NormalizeNaming(ContainerName, name);
return await Provider.GetOrNullAsync( return await Provider.GetOrNullAsync(
new BlobProviderGetArgs( new BlobProviderGetArgs(
ContainerName, normalizedContainerName,
Configuration, Configuration,
name, normalizedBlobName,
CancellationTokenProvider.FallbackToProvider(cancellationToken) CancellationTokenProvider.FallbackToProvider(cancellationToken)
) )
); );
@ -195,5 +212,28 @@ namespace Volo.Abp.BlobStoring
return CurrentTenant.Id; return CurrentTenant.Id;
} }
protected virtual (string, string) NormalizeNaming(string containerName, string blobName)
{
if (!Configuration.NamingNormalizers.Any())
{
return (containerName, blobName);
}
using (var scope = ServiceProvider.CreateScope())
{
foreach (var normalizerType in Configuration.NamingNormalizers)
{
var normalizer = scope.ServiceProvider
.GetRequiredService(normalizerType)
.As<IBlobNamingNormalizer>();
containerName = normalizer.NormalizeContainerName(containerName);
blobName = normalizer.NormalizeBlobName(blobName);
}
return (containerName, blobName);
}
}
} }
} }

10
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using JetBrains.Annotations; using JetBrains.Annotations;
using Volo.Abp.Collections;
namespace Volo.Abp.BlobStoring namespace Volo.Abp.BlobStoring
{ {
@ -18,17 +19,20 @@ namespace Volo.Abp.BlobStoring
/// then the container is shared by all tenants in the system. /// then the container is shared by all tenants in the system.
/// ///
/// This can be <code>true</code> even if your application is not multi-tenant. /// This can be <code>true</code> even if your application is not multi-tenant.
/// ///
/// Default: true. /// Default: true.
/// </summary> /// </summary>
public bool IsMultiTenant { get; set; } = true; public bool IsMultiTenant { get; set; } = true;
public ITypeList<IBlobNamingNormalizer> NamingNormalizers { get; }
[NotNull] private readonly Dictionary<string, object> _properties; [NotNull] private readonly Dictionary<string, object> _properties;
[CanBeNull] private readonly BlobContainerConfiguration _fallbackConfiguration; [CanBeNull] private readonly BlobContainerConfiguration _fallbackConfiguration;
public BlobContainerConfiguration(BlobContainerConfiguration fallbackConfiguration = null) public BlobContainerConfiguration(BlobContainerConfiguration fallbackConfiguration = null)
{ {
NamingNormalizers = new TypeList<IBlobNamingNormalizer>();
_fallbackConfiguration = fallbackConfiguration; _fallbackConfiguration = fallbackConfiguration;
_properties = new Dictionary<string, object>(); _properties = new Dictionary<string, object>();
} }
@ -68,4 +72,4 @@ namespace Volo.Abp.BlobStoring
return this; return this;
} }
} }
} }

24
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs

@ -1,4 +1,5 @@
using Volo.Abp.DependencyInjection; using System;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading; using Volo.Abp.Threading;
@ -7,36 +8,41 @@ namespace Volo.Abp.BlobStoring
public class BlobContainerFactory : IBlobContainerFactory, ITransientDependency public class BlobContainerFactory : IBlobContainerFactory, ITransientDependency
{ {
protected IBlobProviderSelector ProviderSelector { get; } protected IBlobProviderSelector ProviderSelector { get; }
protected IBlobContainerConfigurationProvider ConfigurationProvider { get; } protected IBlobContainerConfigurationProvider ConfigurationProvider { get; }
protected ICurrentTenant CurrentTenant { get; } protected ICurrentTenant CurrentTenant { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; } protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IServiceProvider ServiceProvider { get; }
public BlobContainerFactory( public BlobContainerFactory(
IBlobContainerConfigurationProvider configurationProvider, IBlobContainerConfigurationProvider configurationProvider,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
ICancellationTokenProvider cancellationTokenProvider, ICancellationTokenProvider cancellationTokenProvider,
IBlobProviderSelector providerSelector) IBlobProviderSelector providerSelector,
IServiceProvider serviceProvider)
{ {
ConfigurationProvider = configurationProvider; ConfigurationProvider = configurationProvider;
CurrentTenant = currentTenant; CurrentTenant = currentTenant;
CancellationTokenProvider = cancellationTokenProvider; CancellationTokenProvider = cancellationTokenProvider;
ProviderSelector = providerSelector; ProviderSelector = providerSelector;
ServiceProvider = serviceProvider;
} }
public virtual IBlobContainer Create(string name) public virtual IBlobContainer Create(string name)
{ {
var configuration = ConfigurationProvider.Get(name); var configuration = ConfigurationProvider.Get(name);
return new BlobContainer( return new BlobContainer(
name, name,
configuration, configuration,
ProviderSelector.Get(name), ProviderSelector.Get(name),
CurrentTenant, CurrentTenant,
CancellationTokenProvider CancellationTokenProvider,
ServiceProvider
); );
} }
} }
} }

9
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobNamingNormalizer.cs

@ -0,0 +1,9 @@
namespace Volo.Abp.BlobStoring
{
public interface IBlobNamingNormalizer
{
string NormalizeContainerName(string containerName);
string NormalizeBlobName(string blobName);
}
}

3
framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

25
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Volo.Abp.Caching.StackExchangeRedis</AssemblyName>
<PackageId>Volo.Abp.Caching.StackExchangeRedis</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Caching\Volo.Abp.Caching.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="3.1.5" />
</ItemGroup>
</Project>

30
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs

@ -0,0 +1,30 @@
using System;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Modularity;
namespace Volo.Abp.Caching.StackExchangeRedis
{
[DependsOn(
typeof(AbpCachingModule)
)]
public class AbpCachingStackExchangeRedisModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddStackExchangeRedisCache(options =>
{
var redisConfiguration = configuration["Redis:Configuration"];
if (!redisConfiguration.IsNullOrEmpty())
{
options.Configuration = configuration["Redis:Configuration"];
}
});
context.Services.Replace(ServiceDescriptor.Singleton<IDistributedCache, AbpRedisCache>());
}
}
}

294
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs

@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Caching.StackExchangeRedis
{
[DisableConventionalRegistration]
public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems
{
protected static readonly string SetScript;
protected static readonly string AbsoluteExpirationKey;
protected static readonly string SlidingExpirationKey;
protected static readonly string DataKey;
protected static readonly long NotPresent;
private static readonly FieldInfo RedisDatabaseField;
private static readonly MethodInfo ConnectMethod;
private static readonly MethodInfo ConnectAsyncMethod;
private static readonly MethodInfo MapMetadataMethod;
private static readonly MethodInfo GetAbsoluteExpirationMethod;
private static readonly MethodInfo GetExpirationInSecondsMethod;
protected IDatabase RedisDatabase => GetRedisDatabase();
private IDatabase _redisDatabase;
protected string Instance { get; }
static AbpRedisCache()
{
var type = typeof(RedisCache);
RedisDatabaseField = type.GetField("_cache", BindingFlags.Instance | BindingFlags.NonPublic);
ConnectMethod = type.GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic);
ConnectAsyncMethod = type.GetMethod("ConnectAsync", BindingFlags.Instance | BindingFlags.NonPublic);
MapMetadataMethod = type.GetMethod("MapMetadata", BindingFlags.Instance | BindingFlags.NonPublic);
GetAbsoluteExpirationMethod = type.GetMethod("GetAbsoluteExpiration", BindingFlags.Static | BindingFlags.NonPublic);
GetExpirationInSecondsMethod = type.GetMethod("GetExpirationInSeconds", BindingFlags.Static | BindingFlags.NonPublic);
SetScript = type.GetField("SetScript", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
AbsoluteExpirationKey = type.GetField("AbsoluteExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
SlidingExpirationKey = type.GetField("SlidingExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
DataKey = type.GetField("DataKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
NotPresent = type.GetField("NotPresent", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).To<int>();
}
public AbpRedisCache(IOptions<RedisCacheOptions> optionsAccessor)
: base(optionsAccessor)
{
Instance = optionsAccessor.Value.InstanceName ?? string.Empty;
}
protected virtual void Connect()
{
if (GetRedisDatabase() != null)
{
return;
}
ConnectMethod.Invoke(this, Array.Empty<object>());
}
protected virtual Task ConnectAsync(CancellationToken token = default)
{
if (GetRedisDatabase() != null)
{
return Task.CompletedTask;
}
return (Task) ConnectAsyncMethod.Invoke(this, new object[] {token});
}
public byte[][] GetMany(
IEnumerable<string> keys)
{
keys = Check.NotNull(keys, nameof(keys));
return GetAndRefreshMany(keys, true);
}
public async Task<byte[][]> GetManyAsync(
IEnumerable<string> keys,
CancellationToken token = default)
{
keys = Check.NotNull(keys, nameof(keys));
return await GetAndRefreshManyAsync(keys, true, token);
}
public void SetMany(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options)
{
Connect();
Task.WaitAll(PipelineSetMany(items, options));
}
public async Task SetManyAsync(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options,
CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
await ConnectAsync(token);
await Task.WhenAll(PipelineSetMany(items, options));
}
protected virtual byte[][] GetAndRefreshMany(
IEnumerable<string> keys,
bool getData)
{
Connect();
var keyArray = keys.Select(key => Instance + key).ToArray();
RedisValue[][] results;
if (getData)
{
results = RedisDatabase.HashMemberGetMany(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey, DataKey);
}
else
{
results = RedisDatabase.HashMemberGetMany(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey);
}
Task.WaitAll(PipelineRefreshManyAndOutData(keyArray, results, out var bytes));
return bytes;
}
protected virtual async Task<byte[][]> GetAndRefreshManyAsync(
IEnumerable<string> keys,
bool getData,
CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
await ConnectAsync(token);
var keyArray = keys.Select(key => Instance + key).ToArray();
RedisValue[][] results;
if (getData)
{
results = await RedisDatabase.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey, DataKey);
}
else
{
results = await RedisDatabase.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey);
}
await Task.WhenAll(PipelineRefreshManyAndOutData(keyArray, results, out var bytes));
return bytes;
}
protected virtual Task[] PipelineRefreshManyAndOutData(
string[] keys,
RedisValue[][] results,
out byte[][] bytes)
{
bytes = new byte[keys.Length][];
var tasks = new Task[keys.Length];
for (var i = 0; i < keys.Length; i++)
{
if (results[i].Length >= 2)
{
MapMetadata(results[i], out DateTimeOffset? absExpr, out TimeSpan? sldExpr);
if (sldExpr.HasValue)
{
TimeSpan? expr;
if (absExpr.HasValue)
{
var relExpr = absExpr.Value - DateTimeOffset.Now;
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr;
}
else
{
expr = sldExpr;
}
tasks[i] = RedisDatabase.KeyExpireAsync(keys[i], expr);
}
else
{
tasks[i] = Task.CompletedTask;
}
}
if (results[i].Length >= 3 && results[i][2].HasValue)
{
bytes[i] = results[i][2];
}
else
{
bytes[i] = null;
}
}
return tasks;
}
protected virtual Task[] PipelineSetMany(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options)
{
items = Check.NotNull(items, nameof(items));
options = Check.NotNull(options, nameof(options));
var itemArray = items.ToArray();
var tasks = new Task[itemArray.Length];
var creationTime = DateTimeOffset.UtcNow;
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options);
for (var i = 0; i < itemArray.Length; i++)
{
tasks[i] = RedisDatabase.ScriptEvaluateAsync(SetScript, new RedisKey[] {Instance + itemArray[i].Key},
new RedisValue[]
{
absoluteExpiration?.Ticks ?? NotPresent,
options.SlidingExpiration?.Ticks ?? NotPresent,
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
itemArray[i].Value
});
}
return tasks;
}
protected virtual void MapMetadata(
RedisValue[] results,
out DateTimeOffset? absoluteExpiration,
out TimeSpan? slidingExpiration)
{
var parameters = new object[] {results, null, null};
MapMetadataMethod.Invoke(this, parameters);
absoluteExpiration = (DateTimeOffset?) parameters[1];
slidingExpiration = (TimeSpan?) parameters[2];
}
protected virtual long? GetExpirationInSeconds(
DateTimeOffset creationTime,
DateTimeOffset? absoluteExpiration,
DistributedCacheEntryOptions options)
{
return (long?) GetExpirationInSecondsMethod.Invoke(null,
new object[] {creationTime, absoluteExpiration, options});
}
protected virtual DateTimeOffset? GetAbsoluteExpiration(
DateTimeOffset creationTime,
DistributedCacheEntryOptions options)
{
return (DateTimeOffset?) GetAbsoluteExpirationMethod.Invoke(null, new object[] {creationTime, options});
}
private IDatabase GetRedisDatabase()
{
if (_redisDatabase == null)
{
_redisDatabase = RedisDatabaseField.GetValue(this) as IDatabase;
}
return _redisDatabase;
}
}
}

47
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs

@ -0,0 +1,47 @@
using System.Linq;
using System.Threading.Tasks;
using StackExchange.Redis;
namespace Volo.Abp.Caching.StackExchangeRedis
{
public static class AbpRedisExtensions
{
public static RedisValue[][] HashMemberGetMany(
this IDatabase cache,
string[] keys,
params string[] members)
{
var tasks = new Task<RedisValue[]>[keys.Length];
var fields = members.Select(member => (RedisValue) member).ToArray();
var results = new RedisValue[keys.Length][];
for (var i = 0; i < keys.Length; i++)
{
tasks[i] = cache.HashGetAsync((RedisKey) keys[i], fields);
}
for (var i = 0; i < tasks.Length; i++)
{
results[i] = cache.Wait(tasks[i]);
}
return results;
}
public static async Task<RedisValue[][]> HashMemberGetManyAsync(
this IDatabase cache,
string[] keys,
params string[] members)
{
var tasks = new Task<RedisValue[]>[keys.Length];
var fields = members.Select(member => (RedisValue) member).ToArray();
for (var i = 0; i < keys.Length; i++)
{
tasks[i] = cache.HashGetAsync((RedisKey) keys[i], fields);
}
return await Task.WhenAll(tasks);
}
}
}

393
framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs

@ -1,6 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -28,16 +31,16 @@ namespace Volo.Abp.Caching
IDistributedCacheSerializer serializer, IDistributedCacheSerializer serializer,
IDistributedCacheKeyNormalizer keyNormalizer, IDistributedCacheKeyNormalizer keyNormalizer,
IHybridServiceScopeFactory serviceScopeFactory) : base( IHybridServiceScopeFactory serviceScopeFactory) : base(
distributedCacheOption: distributedCacheOption, distributedCacheOption: distributedCacheOption,
cache: cache, cache: cache,
cancellationTokenProvider: cancellationTokenProvider, cancellationTokenProvider: cancellationTokenProvider,
serializer: serializer, serializer: serializer,
keyNormalizer: keyNormalizer, keyNormalizer: keyNormalizer,
serviceScopeFactory: serviceScopeFactory) serviceScopeFactory: serviceScopeFactory)
{ {
} }
} }
/// <summary> /// <summary>
/// Represents a distributed cache of <typeparamref name="TCacheItem" /> type. /// Represents a distributed cache of <typeparamref name="TCacheItem" /> type.
/// Uses a generic cache key type of <typeparamref name="TCacheKey" /> type. /// Uses a generic cache key type of <typeparamref name="TCacheKey" /> type.
@ -148,19 +151,151 @@ namespace Volo.Abp.Caching
{ {
if (hideErrors == true) if (hideErrors == true)
{ {
AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); HandleException(ex);
return null; return null;
} }
throw; throw;
} }
if (cachedBytes == null) return ToCacheItem(cachedBytes);
}
public virtual KeyValuePair<TCacheKey, TCacheItem>[] GetMany(
IEnumerable<TCacheKey> keys,
bool? hideErrors = null)
{
var keyArray = keys.ToArray();
var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
if (cacheSupportsMultipleItems == null)
{ {
return null; return GetManyFallback(
keyArray,
hideErrors
);
} }
return Serializer.Deserialize<TCacheItem>(cachedBytes); hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
byte[][] cachedBytes;
try
{
cachedBytes = cacheSupportsMultipleItems.GetMany(keyArray.Select(NormalizeKey));
}
catch (Exception ex)
{
if (hideErrors == true)
{
HandleException(ex);
return ToCacheItemsWithDefaultValues(keyArray);
}
throw;
}
return ToCacheItems(cachedBytes, keyArray);
}
protected virtual KeyValuePair<TCacheKey, TCacheItem>[] GetManyFallback(
TCacheKey[] keys,
bool? hideErrors = null)
{
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
return keys
.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(
key,
Get(key, hideErrors: false)
)
).ToArray();
}
catch (Exception ex)
{
if (hideErrors == true)
{
HandleException(ex);
return ToCacheItemsWithDefaultValues(keys);
}
throw;
}
}
public virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyAsync(
IEnumerable<TCacheKey> keys,
bool? hideErrors = null,
CancellationToken token = default)
{
var keyArray = keys.ToArray();
var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
if (cacheSupportsMultipleItems == null)
{
return await GetManyFallbackAsync(
keyArray,
hideErrors,
token
);
}
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
byte[][] cachedBytes;
try
{
cachedBytes = await cacheSupportsMultipleItems.GetManyAsync(
keyArray.Select(NormalizeKey),
CancellationTokenProvider.FallbackToProvider(token)
);
}
catch (Exception ex)
{
if (hideErrors == true)
{
await HandleExceptionAsync(ex);
return ToCacheItemsWithDefaultValues(keyArray);
}
throw;
}
return ToCacheItems(cachedBytes, keyArray);
}
protected virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyFallbackAsync(
TCacheKey[] keys,
bool? hideErrors = null,
CancellationToken token = default)
{
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();
foreach (var key in keys)
{
result.Add(new KeyValuePair<TCacheKey, TCacheItem>(
key,
await GetAsync(key, false, token))
);
}
return result.ToArray();
}
catch (Exception ex)
{
if (hideErrors == true)
{
await HandleExceptionAsync(ex);
return ToCacheItemsWithDefaultValues(keys);
}
throw;
}
} }
/// <summary> /// <summary>
@ -307,7 +442,7 @@ namespace Volo.Abp.Caching
{ {
if (hideErrors == true) if (hideErrors == true)
{ {
AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); HandleException(ex);
return; return;
} }
@ -354,14 +489,156 @@ namespace Volo.Abp.Caching
} }
} }
/// <summary> public void SetMany(
/// Refreshes the cache value of the given key, and resets its sliding expiration timeout. IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
/// </summary> DistributedCacheEntryOptions options = null,
/// <param name="key">The key of cached item to be retrieved from the cache.</param> bool? hideErrors = null)
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param> {
var itemsArray = items.ToArray();
var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
if (cacheSupportsMultipleItems == null)
{
SetManyFallback(
itemsArray,
options,
hideErrors
);
return;
}
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
cacheSupportsMultipleItems.SetMany(
ToRawCacheItems(itemsArray),
options ?? DefaultCacheOptions
);
}
catch (Exception ex)
{
if (hideErrors == true)
{
HandleException(ex);
return;
}
throw;
}
}
protected virtual void SetManyFallback(
KeyValuePair<TCacheKey, TCacheItem>[] items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null)
{
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
foreach (var item in items)
{
Set(
item.Key,
item.Value,
options: options,
hideErrors: false
);
}
}
catch (Exception ex)
{
if (hideErrors == true)
{
HandleException(ex);
return;
}
throw;
}
}
public virtual async Task SetManyAsync(
IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null,
CancellationToken token = default)
{
var itemsArray = items.ToArray();
var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
if (cacheSupportsMultipleItems == null)
{
await SetManyFallbackAsync(
itemsArray,
options,
hideErrors,
token
);
return;
}
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
await cacheSupportsMultipleItems.SetManyAsync(
ToRawCacheItems(itemsArray),
options ?? DefaultCacheOptions,
CancellationTokenProvider.FallbackToProvider(token)
);
}
catch (Exception ex)
{
if (hideErrors == true)
{
await HandleExceptionAsync(ex);
return;
}
throw;
}
}
protected virtual async Task SetManyFallbackAsync(
KeyValuePair<TCacheKey, TCacheItem>[] items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null,
CancellationToken token = default)
{
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
foreach (var item in items)
{
await SetAsync(
item.Key,
item.Value,
options: options,
hideErrors: false,
token: token
);
}
}
catch (Exception ex)
{
if (hideErrors == true)
{
await HandleExceptionAsync(ex);
return;
}
throw;
}
}
public virtual void Refresh( public virtual void Refresh(
TCacheKey key, bool? TCacheKey key, bool?
hideErrors = null) hideErrors = null)
{ {
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
@ -373,20 +650,14 @@ namespace Volo.Abp.Caching
{ {
if (hideErrors == true) if (hideErrors == true)
{ {
AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); HandleException(ex);
return; return;
} }
throw; throw;
} }
} }
/// <summary>
/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
/// </summary>
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
public virtual async Task RefreshAsync( public virtual async Task RefreshAsync(
TCacheKey key, TCacheKey key,
bool? hideErrors = null, bool? hideErrors = null,
@ -410,11 +681,6 @@ namespace Volo.Abp.Caching
} }
} }
/// <summary>
/// Removes the cache item for given key from cache.
/// </summary>
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
public virtual void Remove( public virtual void Remove(
TCacheKey key, TCacheKey key,
bool? hideErrors = null) bool? hideErrors = null)
@ -429,7 +695,7 @@ namespace Volo.Abp.Caching
{ {
if (hideErrors == true) if (hideErrors == true)
{ {
AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); HandleException(ex);
return; return;
} }
@ -437,13 +703,6 @@ namespace Volo.Abp.Caching
} }
} }
/// <summary>
/// Removes the cache item for given key from cache.
/// </summary>
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
public virtual async Task RemoveAsync( public virtual async Task RemoveAsync(
TCacheKey key, TCacheKey key,
bool? hideErrors = null, bool? hideErrors = null,
@ -467,6 +726,11 @@ namespace Volo.Abp.Caching
} }
} }
protected virtual void HandleException(Exception ex)
{
AsyncHelper.RunSync(() => HandleExceptionAsync(ex));
}
protected virtual async Task HandleExceptionAsync(Exception ex) protected virtual async Task HandleExceptionAsync(Exception ex)
{ {
Logger.LogException(ex, LogLevel.Warning); Logger.LogException(ex, LogLevel.Warning);
@ -478,5 +742,56 @@ namespace Volo.Abp.Caching
.NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning)); .NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning));
} }
} }
protected virtual KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItems(byte[][] itemBytes, TCacheKey[] itemKeys)
{
if (itemBytes.Length != itemKeys.Length)
{
throw new AbpException("count of the item bytes should be same with the count of the given keys");
}
var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();
for (int i = 0; i < itemKeys.Length; i++)
{
result.Add(
new KeyValuePair<TCacheKey, TCacheItem>(
itemKeys[i],
ToCacheItem(itemBytes[i])
)
);
}
return result.ToArray();
}
[CanBeNull]
protected virtual TCacheItem ToCacheItem([CanBeNull] byte[] bytes)
{
if (bytes == null)
{
return null;
}
return Serializer.Deserialize<TCacheItem>(bytes);
}
protected virtual KeyValuePair<string, byte[]>[] ToRawCacheItems(KeyValuePair<TCacheKey, TCacheItem>[] items)
{
return items
.Select(i => new KeyValuePair<string, byte[]>(
NormalizeKey(i.Key),
Serializer.Serialize(i.Value)
)
).ToArray();
}
private static KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItemsWithDefaultValues(TCacheKey[] keys)
{
return keys
.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(key, default))
.ToArray();
}
} }
} }

30
framework/src/Volo.Abp.Caching/Volo/Abp/Caching/ICacheSupportsMultipleItems.cs

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
namespace Volo.Abp.Caching
{
public interface ICacheSupportsMultipleItems
{
byte[][] GetMany(
IEnumerable<string> keys
);
Task<byte[][]> GetManyAsync(
IEnumerable<string> keys,
CancellationToken token = default
);
void SetMany(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options
);
Task SetManyAsync(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options,
CancellationToken token = default
);
}
}

63
framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -34,6 +35,39 @@ namespace Volo.Abp.Caching
TCacheKey key, TCacheKey key,
bool? hideErrors = null bool? hideErrors = null
); );
/// <summary>
/// Gets multiple cache items with the given keys.
///
/// The returned list contains exactly the same count of items specified in the given keys.
/// An item in the return list can not be null, but an item in the list has null value
/// if the related key not found in the cache.
/// </summary>
/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// <returns>List of cache items.</returns>
KeyValuePair<TCacheKey, TCacheItem>[] GetMany(
IEnumerable<TCacheKey> keys,
bool? hideErrors = null
);
/// <summary>
/// Gets multiple cache items with the given keys.
///
/// The returned list contains exactly the same count of items specified in the given keys.
/// An item in the return list can not be null, but an item in the list has null value
/// if the related key not found in the cache.
///
/// </summary>
/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// /// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
/// <returns>List of cache items.</returns>
Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyAsync(
IEnumerable<TCacheKey> keys,
bool? hideErrors = null,
CancellationToken token = default
);
/// <summary> /// <summary>
/// Gets a cache item with the given key. If no cache item is found for the given key then returns null. /// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
@ -113,6 +147,35 @@ namespace Volo.Abp.Caching
CancellationToken token = default CancellationToken token = default
); );
/// <summary>
/// Sets multiple cache items.
/// Based on the implementation, this can be more efficient than setting multiple items individually.
/// </summary>
/// <param name="items">Items to set on the cache</param>
/// <param name="options">The cache options for the value.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
void SetMany(
IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null
);
/// <summary>
/// Sets multiple cache items.
/// Based on the implementation, this can be more efficient than setting multiple items individually.
/// </summary>
/// <param name="items">Items to set on the cache</param>
/// <param name="options">The cache options for the value.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
Task SetManyAsync(
IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null,
CancellationToken token = default
);
/// <summary> /// <summary>
/// Refreshes the cache value of the given key, and resets its sliding expiration timeout. /// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
/// </summary> /// </summary>

38
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs

@ -51,17 +51,17 @@ namespace Volo.Abp.Cli.Commands
var apiUrl = commandLineArgs.Options.GetOrNull(Options.ApiUrl.Short, Options.ApiUrl.Long); var apiUrl = commandLineArgs.Options.GetOrNull(Options.ApiUrl.Short, Options.ApiUrl.Long);
if (string.IsNullOrWhiteSpace(apiUrl)) if (string.IsNullOrWhiteSpace(apiUrl))
{ {
var environmentJson = File.ReadAllText("src/environments/environment.ts").Split("export const environment = ")[1].Replace(";", " "); var environmentJson = File.ReadAllText("projects/dev-app/src/environments/environment.ts").Split("export const environment = ")[1].Replace(";", " ");
var environment = JObject.Parse(environmentJson); var environment = JObject.Parse(environmentJson);
apiUrl = environment["apis"]["default"]["url"].ToString(); apiUrl = environment["apis"]["default"]["url"].ToString();
} }
apiUrl += "/api/abp/api-definition?IncludeTypes=true"; apiUrl = apiUrl.EnsureEndsWith('/') + "api/abp/api-definition?IncludeTypes=true";
var uiFramework = GetUiFramework(commandLineArgs); var uiFramework = GetUiFramework(commandLineArgs);
output = commandLineArgs.Options.GetOrNull(Options.Output.Short, Options.Output.Long); output = commandLineArgs.Options.GetOrNull(Options.Output.Short, Options.Output.Long);
if (!string.IsNullOrWhiteSpace(output) && !(output.EndsWith("/") || output.EndsWith("\\"))) if (!string.IsNullOrWhiteSpace(output) && !(output.EndsWith("/") || output.EndsWith("\\")))
{ {
output += "/"; output += "/";
} }
@ -110,7 +110,7 @@ namespace Volo.Abp.Cli.Commands
if (rootPath != "app") if (rootPath != "app")
{ {
Logger.LogInformation($"{rootPath} directory is creating"); Logger.LogInformation($"{rootPath} directory is creating");
} }
if (rootPath == "app") if (rootPath == "app")
{ {
@ -469,17 +469,21 @@ namespace Volo.Abp.Cli.Commands
if (returnValueType.Contains("<")) if (returnValueType.Contains("<"))
{ {
returnValueType = returnValueType.Split('<')[1].Split('>')[0]; returnValueType = returnValueType.Split('<')[1].Split('>')[0];
var clrType = Type.GetType(returnValueType, throwOnError: false);
if (clrType != null && TypeHelper.IsPrimitiveExtended(clrType, includeEnums: true))
{
return null;
}
if (returnValueType.StartsWith("Volo.Abp.Application.Dtos") if (returnValueType.StartsWith("Volo.Abp.Application.Dtos")
|| returnValueType.StartsWith("System.Collections") || returnValueType.StartsWith("System.Collections")
|| returnValueType == "System.String" || returnValueType == "System.Void"
|| returnValueType == "System.Void" || returnValueType.Contains("System.Net.HttpStatusCode?")
|| returnValueType.Contains("System.Net.HttpStatusCode?") || returnValueType.Contains("IActionResult")
|| returnValueType.Contains("IActionResult") || returnValueType.Contains("ActionResult")
|| returnValueType.Contains("ActionResult") || returnValueType.Contains("IStringValueType")
|| returnValueType.Contains("IStringValueType") || returnValueType.Contains("IValueValidator")
|| returnValueType.Contains("IValueValidator") )
|| returnValueType.Contains("Guid")
)
{ {
return null; return null;
} }
@ -536,7 +540,7 @@ namespace Volo.Abp.Cli.Commands
modelFileText.AppendLine(Environment.NewLine); modelFileText.AppendLine(Environment.NewLine);
modelFileText.AppendLine($"import {{ {baseTypeName} }} from '{baseTypeKebabCase}';"); modelFileText.AppendLine($"import {{ {baseTypeName} }} from '{baseTypeKebabCase}';");
extends = "extends " + (!string.IsNullOrWhiteSpace(customBaseTypeName) ? customBaseTypeName : baseTypeName); extends = "extends " + (!string.IsNullOrWhiteSpace(customBaseTypeName) ? customBaseTypeName : baseTypeName);
var modelIndex = CreateType(data, baseType, rootPath, modelIndexList, controllerPathName); var modelIndex = CreateType(data, baseType, rootPath, modelIndexList, controllerPathName);
@ -628,7 +632,7 @@ namespace Volo.Abp.Cli.Commands
{ {
from = "./" + propertyTypeKebabCase; from = "./" + propertyTypeKebabCase;
} }
modelFileText.Insert(0, $"import {{ {propertyType} }} from '{from}';"); modelFileText.Insert(0, $"import {{ {propertyType} }} from '{from}';");
modelFileText.Insert(0, Environment.NewLine); modelFileText.Insert(0, Environment.NewLine);
modelIndexList.Add(modelIndex); modelIndexList.Add(modelIndex);
@ -816,4 +820,4 @@ namespace Volo.Abp.Cli.Commands
public bool IsOptional { get; set; } public bool IsOptional { get; set; }
public string BindingSourceId { get; set; } public string BindingSourceId { get; set; }
} }
} }

47
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs

@ -59,15 +59,33 @@ namespace Volo.Abp.Cli.Commands
return; return;
} }
var result = CmdHelper.RunCmd("dotnet tool install " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g"); try
{
var result = CmdHelper.RunCmd("dotnet tool install " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
if (result == 0) if (result == 0)
{
Logger.LogInformation("ABP Suite has been successfully installed.");
Logger.LogInformation("You can run it with the CLI command \"abp suite\"");
}
else
{
ShowSuiteManualInstallCommand();
}
}
catch (Exception e)
{ {
Logger.LogInformation("ABP Suite has been successfully installed."); Logger.LogError("Couldn't install ABP Suite." + e.Message);
Logger.LogInformation("You can run it with the CLI command \"abp suite\""); ShowSuiteManualInstallCommand();
} }
} }
private void ShowSuiteManualInstallCommand()
{
Logger.LogInformation("You can also run the following command to install ABP Suite.");
Logger.LogInformation("dotnet tool install -g Volo.Abp.Suite");
}
private async Task UpdateSuiteAsync() private async Task UpdateSuiteAsync()
{ {
var nugetIndexUrl = await _nuGetIndexUrlService.GetAsync(); var nugetIndexUrl = await _nuGetIndexUrlService.GetAsync();
@ -77,7 +95,26 @@ namespace Volo.Abp.Cli.Commands
return; return;
} }
CmdHelper.RunCmd("dotnet tool update " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g"); try
{
var result = CmdHelper.RunCmd("dotnet tool update " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
if (result != 0)
{
ShowSuiteManualUpdateCommand();
}
}
catch (Exception ex)
{
Logger.LogError("Couldn't update ABP Suite." + ex.Message);
ShowSuiteManualUpdateCommand();
}
}
private void ShowSuiteManualUpdateCommand()
{
Logger.LogError("You can also run the following command to update ABP Suite.");
Logger.LogError("dotnet tool update -g Volo.Abp.Suite");
} }
private static void RemoveSuite() private static void RemoveSuite()

6
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AngularEnvironmentFilePortChangeForSeparatedIdentityServersStep.cs

@ -10,9 +10,9 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.App
{ {
var fileEntries = context.Files.Where(x => var fileEntries = context.Files.Where(x =>
!x.IsDirectory && !x.IsDirectory &&
(x.Name.EndsWith("angular/src/environments/environment.ts", StringComparison.InvariantCultureIgnoreCase) || (x.Name.EndsWith("angular/projects/dev-app/src/environments/environment.ts", StringComparison.InvariantCultureIgnoreCase) ||
x.Name.EndsWith("angular/src/environments/environment.hmr.ts", StringComparison.InvariantCultureIgnoreCase) || x.Name.EndsWith("angular/projects/dev-app/src/environments/environment.hmr.ts", StringComparison.InvariantCultureIgnoreCase) ||
x.Name.EndsWith("angular/src/environments/environment.prod.ts", StringComparison.InvariantCultureIgnoreCase)) x.Name.EndsWith("angular/projects/dev-app/src/environments/environment.prod.ts", StringComparison.InvariantCultureIgnoreCase))
) )
.ToList(); .ToList();

91
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs

@ -54,11 +54,12 @@ namespace Volo.Abp.Cli.ProjectModification
_npmGlobalPackagesChecker.Check(); _npmGlobalPackagesChecker.Check();
var packagesUpdated = new ConcurrentDictionary<string, bool>(); var packagesUpdated = new ConcurrentDictionary<string, bool>();
async Task UpdateAsync(string file) async Task UpdateAsync(string file)
{ {
var updated = await UpdatePackagesInFile(file, includePreviews, switchToStable); var updated = await UpdatePackagesInFile(file, includePreviews, switchToStable);
packagesUpdated.TryAdd(file, updated); packagesUpdated.TryAdd(file, updated);
}; }
Task.WaitAll(fileList.Select(UpdateAsync).ToArray()); Task.WaitAll(fileList.Select(UpdateAsync).ToArray());
@ -66,23 +67,20 @@ namespace Volo.Abp.Cli.ProjectModification
{ {
var fileDirectory = Path.GetDirectoryName(file.Key).EnsureEndsWith(Path.DirectorySeparatorChar); var fileDirectory = Path.GetDirectoryName(file.Key).EnsureEndsWith(Path.DirectorySeparatorChar);
if (IsAngularProject(fileDirectory)) if (includePreviews)
{ {
if (includePreviews) await CreateNpmrcFileAsync(Path.GetDirectoryName(file.Key));
{ RunNpmInstall(fileDirectory);
await CreateNpmrcFileAsync(Path.GetDirectoryName(file.Key)); }
} else if (switchToStable)
else if (switchToStable) {
{ await DeleteNpmrcFileAsync(Path.GetDirectoryName(file.Key));
await DeleteNpmrcFileAsync(Path.GetDirectoryName(file.Key)); RunYarn(fileDirectory);
}
} }
RunYarn(fileDirectory);
if (!IsAngularProject(fileDirectory)) if (!IsAngularProject(fileDirectory))
{ {
Thread.Sleep(500); Thread.Sleep(1000);
RunGulp(fileDirectory); RunGulp(fileDirectory);
} }
} }
@ -166,7 +164,8 @@ namespace Volo.Abp.Cli.ProjectModification
return File.Exists(Path.Combine(fileDirectory, "angular.json")); return File.Exists(Path.Combine(fileDirectory, "angular.json"));
} }
protected virtual async Task<bool> UpdatePackagesInFile(string filePath, bool includePreviews = false, bool switchToStable = false) protected virtual async Task<bool> UpdatePackagesInFile(string filePath, bool includePreviews = false,
bool switchToStable = false)
{ {
var packagesUpdated = false; var packagesUpdated = false;
var fileContent = File.ReadAllText(filePath); var fileContent = File.ReadAllText(filePath);
@ -201,67 +200,54 @@ namespace Volo.Abp.Cli.ProjectModification
bool includePreviews = false, bool includePreviews = false,
bool switchToStable = false) bool switchToStable = false)
{ {
var currentVersion = (string)package.Value; var currentVersion = (string) package.Value;
var version = await GetLatestVersion(package, currentVersion, includePreviews, switchToStable); var version = "";
if (includePreviews || (!switchToStable && currentVersion.Contains("-preview")))
var versionWithPrefix = $"~{version}"; {
version = "preview";
}
else
{
version = await GetLatestVersion(package);
}
if (versionWithPrefix == currentVersion) if (version == currentVersion)
{ {
return false; return false;
} }
package.Value.Replace(versionWithPrefix); package.Value.Replace(version);
Logger.LogInformation($"Updated {package.Name} to {version} in {filePath.Replace(Directory.GetCurrentDirectory(), "")}."); Logger.LogInformation(
$"Updated {package.Name} to {version} in {filePath.Replace(Directory.GetCurrentDirectory(), "")}.");
return true; return true;
} }
protected virtual async Task<string> GetLatestVersion( protected virtual async Task<string> GetLatestVersion(
JProperty package, JProperty package)
string currentVersion,
bool includePreviews = false,
bool switchToStable = false)
{ {
if (_fileVersionStorage.ContainsKey(package.Name)) if (_fileVersionStorage.ContainsKey(package.Name))
{ {
return _fileVersionStorage[package.Name]; return _fileVersionStorage[package.Name];
} }
var newVersion = currentVersion; var newVersion = CmdHelper.RunCmdAndGetOutput($"npm show {package.Name} version");
var newVersionWithPrefix = $"~{newVersion}";
if (includePreviews || (!switchToStable && currentVersion.Contains("-preview"))) _fileVersionStorage[package.Name] = newVersionWithPrefix;
{
if (_myGetApiResponse == null)
{
_myGetApiResponse = await _myGetPackageListFinder.GetPackagesAsync();
}
var mygetPackage = _myGetApiResponse.Packages.FirstOrDefault(p => p.Id == package.Name); return $"~{newVersionWithPrefix}";
if (mygetPackage != null)
{
newVersion = mygetPackage.Versions.Last();
}
}
else
{
newVersion = CmdHelper.RunCmdAndGetOutput($"npm show {package.Name} version");
}
_fileVersionStorage[package.Name] = newVersion;
return newVersion;
} }
protected virtual List<JProperty> GetAbpPackagesFromPackageJson(JObject fileObject) protected virtual List<JProperty> GetAbpPackagesFromPackageJson(JObject fileObject)
{ {
var dependencyList = new[] { "dependencies", "devDependencies", "peerDependencies" }; var dependencyList = new[] {"dependencies", "devDependencies", "peerDependencies"};
var abpPackages = new List<JProperty>(); var abpPackages = new List<JProperty>();
foreach (var dependencyListName in dependencyList) foreach (var dependencyListName in dependencyList)
{ {
var dependencies = (JObject)fileObject[dependencyListName]; var dependencies = (JObject) fileObject[dependencyListName];
if (dependencies == null) if (dependencies == null)
{ {
@ -269,7 +255,8 @@ namespace Volo.Abp.Cli.ProjectModification
} }
var properties = dependencies.Properties().ToList(); var properties = dependencies.Properties().ToList();
abpPackages.AddRange(properties.Where(p => p.Name.StartsWith("@abp/") || p.Name.StartsWith("@volo/")).ToList()); abpPackages.AddRange(properties.Where(p => p.Name.StartsWith("@abp/") || p.Name.StartsWith("@volo/"))
.ToList());
} }
return abpPackages; return abpPackages;
@ -286,5 +273,11 @@ namespace Volo.Abp.Cli.ProjectModification
Logger.LogInformation($"Running Yarn on {fileDirectory}"); Logger.LogInformation($"Running Yarn on {fileDirectory}");
CmdHelper.RunCmd($"cd {fileDirectory} && yarn"); CmdHelper.RunCmd($"cd {fileDirectory} && yarn");
} }
protected virtual void RunNpmInstall(string fileDirectory)
{
Logger.LogInformation($"Running npm install on {fileDirectory}");
CmdHelper.RunCmd($"cd {fileDirectory} && npm install");
}
} }
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save