diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index a5089e06f7..d0d829bbf3 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -13,8 +13,11 @@ "Permission:Edit": "Edit", "Permission:Delete": "Delete", "Permission:Create": "Create", + "Permission:Accounting": "Accounting", + "Permission:Accounting:Quotation": "Quotation", + "Permission:Accounting:Invoice": "Invoice", "Menu:Organizations": "Organizations", - "Menu:Accounting": "Accounting", + "Menu:Accounting": "Accounting", "Menu:Packages": "Packages", "NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Do you confirm that?", "NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Do you confirm that?", @@ -94,13 +97,28 @@ "UsernameOrEmail": "Username or email", "UsernameOrEmailPlaceholder": "Username or email...", "Member": "Member", - "QuotationPurchasedOrderNo": "Quotation Purchased Order No", - "QuotationTime": "Quotation Time", - "CompanyName": "Company Name", - "CompanyAddress": "Company Address", + "PurchaseOrderNo": "Purchase order no", + "QuotationDate": "Quotation date", + "CompanyName": "Company name", + "CompanyAddress": "Company address", "Price": "Price", + "DiscountText": "Discount text", + "DiscountQuantity": "Discount quantity", + "DiscountPrice": "Discount price", + "Quotation": "Quotation", "ExtraText": "Extra Text", "ExtraAmount": "Extra Amount", - "DownloadQuotation": "Download Quotation" + "DownloadQuotation": "Download Quotation", + "Invoice": "Invoice", + "TaxNumber": "Tax Number", + "InvoiceNumber": "Invoice Number", + "InvoiceDate": "Invoice Date", + "Quantity": "Quantity", + "AddProduct": "Add Product", + "AddProductWarning": "You need to add product!", + "TotalPrice": "Total Price", + "Generate": "Generate", + "MissingQuantityField": "The quantity field is required!", + "MissingPriceField": "The Price field is required!" } } \ No newline at end of file diff --git a/common.DotSettings b/common.DotSettings index 0eb4875d49..6f40d029a7 100644 --- a/common.DotSettings +++ b/common.DotSettings @@ -20,5 +20,11 @@ False False SQL + False + False + False + False + False + False True \ No newline at end of file diff --git a/common.props b/common.props index 19b6f81fe6..ab7b86e3b2 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 2.7.0 + 2.8.0 $(NoWarn);CS1591 https://abp.io/assets/abp_nupkg.png https://abp.io diff --git a/docs/en/CLI.md b/docs/en/CLI.md index b687a5244a..75e3e027b4 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -135,6 +135,8 @@ abp update [options] * `--include-previews` or `-p`: Includes preview, beta and rc packages while checking the latest versions. * `--npm`: Only updates NPM packages. * `--nuget`: Only updates NuGet packages. +* `--solution-path` or `-sp`: Specify the solution path. Use the current directory by default +* `--solution-name` or `-sn`: Specify the solution name. Search `*.sln` files in the directory by default. ### switch-to-preview @@ -170,7 +172,11 @@ Some features of the CLI requires to be logged in to abp.io platform. To login w abp login ``` -Notice that, a new login with an already active session, will kill the previous session and creates a new one. +```bash +abp login -p +``` + +Notice that, a new login with an already active session, overwrites the previous session. ### logout diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md index e0772579ec..00e791a94e 100644 --- a/docs/en/Entity-Framework-Core-Migrations.md +++ b/docs/en/Entity-Framework-Core-Migrations.md @@ -546,6 +546,8 @@ Entity extension system solves the main problem of the extra properties: It can All you need to do is to use the `ObjectExtensionManager` to define the extra property as explained above, in the `AppRole` example. Then you can continue to use the same `GetProperty` and `SetProperty` methods defined above to get/set the related property on the entity, but this time stored as a separate field in the database. +See the [entity extension system](Customizing-Application-Modules-Extending-Entities.md) for details. + ###### Creating a New Table Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table. diff --git a/docs/en/Getting-Started.md b/docs/en/Getting-Started.md index 07e55f553e..fa7f47f276 100644 --- a/docs/en/Getting-Started.md +++ b/docs/en/Getting-Started.md @@ -25,6 +25,12 @@ The following tools should be installed on your development machine: * [Node v12+](https://nodejs.org) * [Yarn v1.19+](https://classic.yarnpkg.com/) +{{ if Tiered == "Yes" }} + +* [Redis](https://redis.io/): The applications use Redis as as [distributed cache](../Caching.md). So, you need to have Redis installed & running. + +{{ end }} + > You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core. @@ -400,8 +406,8 @@ Enter **admin** as the username and **1q2w3E*** as the password to login to the The application is up and running. You can continue to develop your application based on this startup template. -> The [application startup template](startup-templates/application/index.md) includes the TenantManagement and Identity modules. +> The [application startup template](Startup-Templates/Application.md) includes the TenantManagement and Identity modules. ## What's next? -[Application development tutorial](tutorials/book-store/part-1.md) +[Application development tutorial](Tutorials/Part-1.md) diff --git a/docs/en/Localization.md b/docs/en/Localization.md index 11b838cf7f..954fe8620f 100644 --- a/docs/en/Localization.md +++ b/docs/en/Localization.md @@ -87,6 +87,21 @@ A JSON localization file content is shown below: * Every localization file should define the `culture` code for the file (like "en" or "en-US"). * `texts` section just contains key-value collection of the localization strings (keys may have spaces too). +### Default Resource + +`AbpLocalizationOptions.DefaultResourceType` can be set to a resource type, so it is used when the localization resource was not specified: + +````csharp +Configure(options => +{ + options.DefaultResourceType = typeof(TestResource); +}); +```` + +> The [application startup template](Startup-Templates/Application.md) sets `DefaultResourceType` to the localization resource of the application. + +See the *Client Side* section below for a use case. + ### Short Localization Resource Name Localization resources are also available in the client (JavaScript) side. So, setting a short name for the localization resource makes it easy to use localization texts. Example: @@ -166,6 +181,10 @@ public class MyService } ```` +##### Format Arguments + +Format arguments can be passed after the localization key. If your message is `Hello {0}, welcome!`, then you can pass the `{0}` argument to the localizer like `_localizer["HelloMessage", "John"]` + #### Simplest Usage In A Razor View/Page ````c# @@ -180,18 +199,47 @@ Refer to the [Microsoft's localization documentation](https://docs.microsoft.com ABP provides JavaScript services to use the same localized texts in the client side. -Get a localization resource: +#### getResource + +`abp.localization.getResource` function is used to get a localization resource: ````js var testResource = abp.localization.getResource('Test'); ```` -Localize a string: +Then you can localize a string based on this resource: ````js var str = testResource('HelloWorld'); ```` +#### localize + +`abp.localization.localize` function is a shortcut where you can both specify the text name and the resource name: + +````js +var str = abp.localization.localize('HelloWorld', 'Test'); +```` + +`HelloWorld` is the text to localize, where `Test` is the localization resource name here. + +If you don't specify the localization resource name, it uses the default localization resource defined on the `AbpLocalizationOptions` (see the *Default Resource* section above). Example: + +````js +var str = abp.localization.localize('HelloWorld'); //uses the default resource +```` + +##### Format Arguments + +If your localized string contains arguments, like `Hello {0}, welcome!`, you can pass arguments to the localization methods. Examples: + +````js +var str1 = abp.localization.getResource('Test')('HelloWelcomeMessage', 'John'); +var str2 = abp.localization.localize('HelloWorld', 'Test', 'John'); +```` + +Both of the samples above produce the output `Hello John, welcome!`. + ## See Also * [Localization in Angular UI](UI/Angular/Localization.md) \ No newline at end of file diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index 6eff328c85..bee229dc27 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -197,8 +197,8 @@ ObjectExtensionManager.Instance "SocialSecurityNumber", options => { - options.ValidationAttributes.Add(new RequiredAttribute()); - options.ValidationAttributes.Add( + options.Attributes.Add(new RequiredAttribute()); + options.Attributes.Add( new StringLengthAttribute(32) { MinimumLength = 6 } @@ -248,12 +248,12 @@ ObjectExtensionManager.Instance objConfig.AddOrUpdateProperty("Password", propertyConfig => { - propertyConfig.ValidationAttributes.Add(new RequiredAttribute()); + propertyConfig.Attributes.Add(new RequiredAttribute()); }); objConfig.AddOrUpdateProperty("PasswordRepeat", propertyConfig => { - propertyConfig.ValidationAttributes.Add(new RequiredAttribute()); + propertyConfig.Attributes.Add(new RequiredAttribute()); }); //Write a common validation logic works on multiple properties diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index a0aa6612e9..51ab468d07 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -38,7 +38,7 @@ Create a new project named `Acme.BookStore` where `Acme` is the company name and #### Create the project -By running the below command, it creates a new ABP project with the database provider `{{DB_Text}}` and UI option `MVC`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document. +By running the below command, it creates a new ABP project with the database provider `{{DB_Text}}` and UI option `{{UI_Value}}`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document. ```bash abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} --mobile none @@ -61,7 +61,7 @@ After creating the project, you need to apply the initial migrations and create To run the project, right click to the {{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} project and click **Set As StartUp Project**. And run the web project by pressing **CTRL+F5** (*without debugging and fast*) or press **F5** (*with debugging and slow*). {{if UI == "NG"}}You will see the Swagger UI for BookStore API.{{end}} -Further information, see the [running the application section](../../Getting-Started-{{if UI == "NG"}}Angular{{else}}AspNetCore-MVC{{end}}-Template#running-the-application).Getting-Started-AspNetCore-MVC-Template#running-the-application +Further information, see the [running the application section](../Getting-Started?UI={{UI}}#run-the-application). ![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png) @@ -335,7 +335,7 @@ INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES ### Create the application service -The next step is to create an [application service](../../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects: +The next step is to create an [application service](../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects: * `Acme.BookStore.Application.Contracts` mainly contains your `DTO`s and application service interfaces. * `Acme.BookStore.Application` contains the implementations of your application services. @@ -874,97 +874,52 @@ We'll see **book-list works!** text on the books page: Run the following command in the terminal to create a new state, named `BooksState`: -![Initial book list page](./images/bookstore-generate-state-books.png) - ```bash -yarn ng generate ngxs-schematic:state books +npx @ngxs/cli --name books --directory src/app/books ``` -* This command creates several new files and updates `app.modules.ts` file to import the `NgxsModule` with the new state. - -#### Get books data from backend - -Create data types to map the data from the backend (you can check Swagger UI or your backend API to see the data format). +* This command creates `books.state.ts` and `books.actions.ts` files in the `src/app/books/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli). -![BookDto properties](./images/bookstore-swagger-book-dto-properties.png) - -Open the `books.ts` file in the `app\store\models` folder and replace the content as below: +Import the `BooksState` to the `app.module.ts` in the `src/app` folder and then add the `BooksState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method. ```js -export namespace Books { - export interface State { - books: Response; - } +// ... +import { BooksState } from './books/state/books.state'; //<== imported BooksState ==> - export interface Response { - items: Book[]; - totalCount: number; - } +@NgModule({ + imports: [ + // other imports - export interface Book { - name: string; - type: BookType; - publishDate: string; - price: number; - lastModificationTime: string; - lastModifierId: string; - creationTime: string; - creatorId: string; - id: string; - } + NgxsModule.forRoot([BooksState]), //<== added BooksState ==> - export enum BookType { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry, - } -} + //other imports + ], + // ... +}) +export class AppModule {} ``` -* Added `Book` interface that represents a book object and `BookType` enum which represents a book category. +#### Generate proxies -#### BooksService +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) -Create a new service, named `BooksService` to perform `HTTP` calls to the server: +Run the following command in the `angular` folder: ```bash -yarn ng generate service books/shared/books +abp generate-proxy --module app ``` -![service-terminal-output](./images/bookstore-service-terminal-output.png) +![Generate proxy command](./images/generate-proxy-command.png) -Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below: +The generated files looks like below: -```js -import { Injectable } from '@angular/core'; -import { RestService } from '@abp/ng.core'; -import { Books } from '../../store/models'; -import { Observable } from 'rxjs'; +![Generated files](./images/generated-proxies.png) -@Injectable({ - providedIn: 'root', -}) -export class BooksService { - constructor(private restService: RestService) {} - - get(): Observable { - return this.restService.request({ - method: 'GET', - url: '/api/app/book' - }); - } -} -``` +#### GetBooks Action -* We added the `get` method to get the list of books by performing an HTTP request to the related endpoint. +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`books.actions.ts` file in `app\store\actions` folder and replace the content below: +Open the `books.actions.ts` file in `app/books/state` folder and replace the content below: ```js export class GetBooks { @@ -974,43 +929,49 @@ export class GetBooks { #### Implement BooksState -Open the `books.state.ts` file in `app\store\states` folder and replace the content below: +Open the `books.state.ts` file in `app/books/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 '../actions/books.actions'; -import { Books } from '../models/books'; -import { BooksService } from '../../books/shared/books.service'; +import { GetBooks } from './books.actions'; +import { BookService } from '../../app/shared/services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { BookDto } from '../../app/shared/models'; -@State({ +export class BooksStateModel { + public book: PagedResultDto; +} + +@State({ name: 'BooksState', - defaults: { books: {} } as Books.State, + defaults: { book: {} } as BooksStateModel, }) @Injectable() export class BooksState { @Selector() - static getBooks(state: Books.State) { - return state.books.items || []; + static getBooks(state: BooksStateModel) { + return state.book.items || []; } - constructor(private booksService: BooksService) {} + constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { - return this.booksService.get().pipe( - tap(booksResponse => { + get(ctx: StateContext) { + return this.bookService.getListByInput().pipe( + tap((booksResponse) => { ctx.patchState({ - books: booksResponse, + book: booksResponse, }); - }), + }) ); } } ``` - -* We added the `GetBooks` action that retrieves the books data via `BooksService` and patches the state. +* We added the book property to BooksStateModel model. +* We added `@Injectable()` decorator to BookState class (Regquired for Ivy to work properly). +* We added the `GetBooks` action that retrieves the books data via `BooksService` that generated via ABP CLI and patches the state. * `NGXS` requires to return the observable without subscribing it in the get function. #### BookListComponent @@ -1019,11 +980,12 @@ Open the `book-list.component.ts` file in `app\books\book-list` folder and repla ```js import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; +import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; @Component({ selector: 'app-book-list', @@ -1032,13 +994,13 @@ import { GetBooks } from '../../store/actions'; }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; loading = false; - constructor(private store: Store) { } + constructor(private store: Store) {} ngOnInit() { this.get(); @@ -1046,9 +1008,10 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } } ``` diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 9565724cb8..cfe9936812 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -456,161 +456,76 @@ 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. -#### Type definition - -Open `books.ts` file in `app\store\models` folder and replace the content as below: - -```js -export namespace Books { - export interface State { - books: Response; - } - - export interface Response { - items: Book[]; - totalCount: number; - } - - export interface Book { - name: string; - type: BookType; - publishDate: string; - price: number; - lastModificationTime: string; - lastModifierId: string; - creationTime: string; - creatorId: string; - id: string; - } - - export enum BookType { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry, - } - - //<== added CreateUpdateBookInput interface ==> - export interface CreateUpdateBookInput { - name: string; - type: BookType; - publishDate: string; - price: number; - } -} -``` - -* We added `CreateUpdateBookInput` interface. -* You can see the properties of this interface from Swagger UI. -* The `CreateUpdateBookInput` interface matches with the `CreateUpdateBookDto` in the backend. - -#### Service method - -Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below: - -```js -import { Injectable } from '@angular/core'; -import { RestService } from '@abp/ng.core'; -import { Books } from '../../store/models'; -import { Observable } from 'rxjs'; - -@Injectable({ - providedIn: 'root', -}) -export class BooksService { - constructor(private restService: RestService) {} - - get(): Observable { - return this.restService.request({ - method: 'GET', - url: '/api/app/book' - }); - } - - //<== added create method ==> - create(createBookInput: Books.CreateUpdateBookInput): Observable { - return this.restService.request({ - method: 'POST', - url: '/api/app/book', - body: createBookInput - }); - } -} -``` - -- We added the `create` method to perform an HTTP Post request to the server. -- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used). - #### State definitions -Open `books.action.ts` in `app\store\actions` folder and replace the content as below: +Open `books.action.ts` in `books\state` folder and replace the content as below: ```js -import { Books } from '../models'; //<== added this line ==> +import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==> export class GetBooks { static readonly type = '[Books] Get'; } -//added CreateUpdateBook class +// added CreateUpdateBook class export class CreateUpdateBook { static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput) { } + constructor(public payload: CreateUpdateBookDto) { } } ``` -* We imported the Books namespace and created the `CreateUpdateBook` action. +* We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action. -Open `books.state.ts` file in `app\store\states` and replace the content as below: +Open `books.state.ts` file in `books\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 } from '../actions/books.actions'; //<== added CreateUpdateBook==> -import { Books } from '../models/books'; -import { BooksService } from '../../books/shared/books.service'; +import { GetBooks, CreateUpdateBook } from './books.actions'; // <== added CreateUpdateBook==> +import { BookService } from '../../app/shared/services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { BookDto } from '../../app/shared/models'; + +export class BooksStateModel { + public book: PagedResultDto; +} -@State({ +@State({ name: 'BooksState', - defaults: { books: {} } as Books.State, + defaults: { book: {} } as BooksStateModel, }) @Injectable() export class BooksState { @Selector() - static getBooks(state: Books.State) { - return state.books.items || []; + static getBooks(state: BooksStateModel) { + return state.book.items || []; } - constructor(private booksService: BooksService) { } + constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { - return this.booksService.get().pipe( - tap(booksResponse => { + get(ctx: StateContext) { + return this.bookService.getListByInput().pipe( + tap((bookResponse) => { ctx.patchState({ - books: booksResponse, + book: bookResponse, }); - }), + }) ); } - //added CreateUpdateBook action listener + // added CreateUpdateBook action listener @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { - return this.booksService.create(action.payload); + save(ctx: StateContext, action: CreateUpdateBook) { + 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. -When the `SaveBook` action dispatched, the save method is being executed. It calls `create` method of the `BooksService`. +When the `SaveBook` action dispatched, the save method is being executed. It calls `createByInput` method of the `BookService`. #### Add a modal to BookListComponent @@ -694,11 +609,12 @@ Open `book-list.component.ts` file in `books\book-list` folder and replace the c ```js import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; +import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; @Component({ selector: 'app-book-list', @@ -707,15 +623,15 @@ import { GetBooks } from '../../store/actions'; }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; loading = false; - isModalOpen = false; //<== added this line ==> + isModalOpen = false; // <== added this line ==> - constructor(private store: Store) { } + constructor(private store: Store) {} ngOnInit() { this.get(); @@ -723,12 +639,13 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } - //added createBook method + // added createBook method createBook() { this.isModalOpen = true; } @@ -749,12 +666,13 @@ Open `book-list.component.ts` file in `app\books\book-list` folder and replace t ```js import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; +import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==> +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==> @Component({ selector: 'app-book-list', @@ -763,17 +681,17 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; loading = false; isModalOpen = false; - form: FormGroup; + form: FormGroup; // <== added this line ==> - constructor(private store: Store, private fb: FormBuilder) { } //<== added FormBuilder ==> + constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==> ngOnInit() { this.get(); @@ -781,9 +699,10 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } createBook() { @@ -791,7 +710,7 @@ export class BookListComponent implements OnInit { this.isModalOpen = true; } - //added buildForm method + // added buildForm method buildForm() { this.form = this.fb.group({ name: ['', Validators.required], @@ -804,6 +723,7 @@ export class BookListComponent implements OnInit { ``` * We imported `FormGroup, FormBuilder and Validators`. +* We added `form: FormGroup` variable. * We injected `fb: FormBuilder` service to the constructor. The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms. * We added `buildForm` method to the end of the file and executed `buildForm()` in the `createBook` method. This method creates a reactive form to be able to create a new book. * The `group` method of `FormBuilder`, `fb` creates a `FormGroup`. @@ -880,35 +800,34 @@ export class BooksModule { } * We imported `NgbDatepickerModule` to be able to use the date picker. - - Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: ```js import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; +import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; 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 ==> @Component({ selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] //<== added this line ==> + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==> }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; //added bookTypeArr array - bookTypeArr = Object.keys(Books.BookType).filter( - bookType => typeof this.booksType[bookType] === 'number' + bookTypeArr = Object.keys(BookType).filter( + (bookType) => typeof this.booksType[bookType] === 'number' ); loading = false; @@ -917,7 +836,7 @@ export class BookListComponent implements OnInit { form: FormGroup; - constructor(private store: Store, private fb: FormBuilder) { } + constructor(private store: Store, private fb: FormBuilder) {} ngOnInit() { this.get(); @@ -925,9 +844,10 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } createBook() { @@ -965,35 +885,16 @@ Now, you can open your browser to see the changes: #### Saving the book -Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book. - -```html - - - - - - -``` - -* This adds a save button to the bottom area of the modal: - -![Save button to the modal](./images/bookstore-new-book-form-v2.png) - Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: ```js import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; +import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==> +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks, CreateUpdateBook } from '../state/books.actions'; // <== added CreateUpdateBook ==> +import { BooksState } from '../state/books.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; @@ -1001,16 +902,17 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; - bookTypeArr = Object.keys(Books.BookType).filter( - bookType => typeof this.booksType[bookType] === 'number' + //added bookTypeArr array + bookTypeArr = Object.keys(BookType).filter( + (bookType) => typeof this.booksType[bookType] === 'number' ); loading = false; @@ -1019,7 +921,7 @@ export class BookListComponent implements OnInit { form: FormGroup; - constructor(private store: Store, private fb: FormBuilder) { } + constructor(private store: Store, private fb: FormBuilder) {} ngOnInit() { this.get(); @@ -1027,9 +929,10 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } createBook() { @@ -1062,37 +965,46 @@ export class BookListComponent implements OnInit { ``` * We imported `CreateUpdateBook`. -* We added `save` method +* We added `save` method -### Updating an existing book - -#### BooksService +Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book. -Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods. +```html + + + + + + +``` -```js -getById(id: string): Observable { - return this.restService.request({ - method: 'GET', - url: `/api/app/book/${id}` - }); -} +Find the `
` tag and replace below content: -update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable { - return this.restService.request({ - method: 'PUT', - url: `/api/app/book/${id}`, - body: updateBookInput - }); -} +```html + ``` + +* We added the `(ngSubmit)="save()"` to `` element to save a new book by pressing the enter. +* We added `abp-button` to the bottom area of the modal to save a new book. + +The final modal UI looks like below: + +![Save button to the modal](./images/bookstore-new-book-form-v2.png) + +### Updating a book + #### CreateUpdateBook action -Open the `books.actions.ts` in `app\store\actions` folder and replace the content as below: +Open the `books.actions.ts` in `books\state` folder and replace the content as below: ```js -import { Books } from '../models'; +import { CreateUpdateBookDto } from '../../app/shared/models'; export class GetBooks { static readonly type = '[Books] Get'; @@ -1100,54 +1012,55 @@ export class GetBooks { export class CreateUpdateBook { static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput, public id?: string) { } //<== added id parameter ==> + constructor(public payload: CreateUpdateBookDto, public id?: string) { } // <== added id parameter ==> } ``` * We added `id` parameter to the `CreateUpdateBook` action's constructor. -Open the `books.state.ts` in `app\store\states` folder and replace the `save` method as below: +Open the `books.state.ts` in `books\state` folder and replace the `save` method as below: ```js @Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { - if (action.id) { - return this.booksService.update(action.payload, action.id); - } else { - return this.booksService.create(action.payload); - } +save(ctx: StateContext, 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\books\book-list` folder and inject `BooksService` dependency by adding it to the constructor and add a variable named `selectedBook`. +Open `book-list.component.ts` in `app\books\book-list` folder and inject `BookService` dependency by adding it to the constructor and add a variable named `selectedBook`. ```js import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; +import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks, CreateUpdateBook } from '../../store/actions'; +import { finalize } from 'rxjs/operators'; +import { BookDto, BookType } from '../../app/shared/models'; +import { GetBooks, CreateUpdateBook } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { BooksService } from '../shared/books.service'; //<== imported BooksService ==> +import { BookService } from '../../app/shared/services'; // <== imported BookService ==> @Component({ selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { @Select(BooksState.getBooks) - books$: Observable; + books$: Observable; - booksType = Books.BookType; + booksType = BookType; - bookTypeArr = Object.keys(Books.BookType).filter( - bookType => typeof this.booksType[bookType] === 'number' + bookTypeArr = Object.keys(BookType).filter( + (bookType) => typeof this.booksType[bookType] === 'number' ); loading = false; @@ -1156,9 +1069,9 @@ export class BookListComponent implements OnInit { form: FormGroup; - selectedBook = {} as Books.Book; //<== declared selectedBook ==> + selectedBook = {} as BookDto; // <== declared selectedBook ==> - constructor(private store: Store, private fb: FormBuilder, private booksService: BooksService) { } + constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==> ngOnInit() { this.get(); @@ -1166,39 +1079,38 @@ export class BookListComponent implements OnInit { get() { this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); } - //<== this method is replaced ==> + // <== this method is replaced ==> createBook() { - this.selectedBook = {} as Books.Book; //<== added ==> + this.selectedBook = {} as BookDto; // <== added ==> this.buildForm(); this.isModalOpen = true; } - //<== added editBook method ==> + // <== added editBook method ==> editBook(id: string) { - this.booksService.getById(id).subscribe(book => { + this.bookService.getById(id).subscribe((book) => { this.selectedBook = book; this.buildForm(); this.isModalOpen = true; }); } - //<== this method is replaced ==> + // <== this method is replaced ==> buildForm() { this.form = this.fb.group({ - name: [this.selectedBook.name || "", Validators.required], + name: [this.selectedBook.name || '', Validators.required], type: [this.selectedBook.type || null, Validators.required], publishDate: [ - this.selectedBook.publishDate - ? new Date(this.selectedBook.publishDate) - : null, - Validators.required + this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, + Validators.required, ], - price: [this.selectedBook.price || null, Validators.required] + price: [this.selectedBook.price || null, Validators.required], }); } @@ -1208,7 +1120,8 @@ export class BookListComponent implements OnInit { } //<== added this.selectedBook.id ==> - this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) + this.store + .dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) .subscribe(() => { this.isModalOpen = false; this.form.reset(); @@ -1218,9 +1131,9 @@ export class BookListComponent implements OnInit { } ``` -* We imported `BooksService`. -* We declared a variable named `selectedBook` as `Books.Book`. -* We injected `BooksService` to the constructor. `BooksService` is being used to retrieve the book data which is being edited. +* We imported `BookService`. +* 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 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. @@ -1298,24 +1211,9 @@ Open `book-list.component.html` in `app\books\book-list` folder and find the ` { - return this.restService.request({ - method: 'DELETE', - url: `/api/app/book/${id}` - }); -} -``` - -* `Delete` method gets `id` parameter and makes a `DELETE` HTTP request to the relevant endpoint. - #### DeleteBook action -Open `books.actions.ts` in `app\store\actions `folder and add an action named `DeleteBook`. +Open `books.actions.ts` in `books\state `folder and add an action named `DeleteBook`. ```js export class DeleteBook { @@ -1324,53 +1222,58 @@ export class DeleteBook { } ``` -Open the `books.state.ts` in `app\store\states` folder and replace the content as below: +Open the `books.state.ts` in `books\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 '../actions/books.actions'; //<== added DeleteBook==> -import { Books } from '../models/books'; -import { BooksService } from '../../books/shared/books.service'; +import { GetBooks, CreateUpdateBook, DeleteBook } from './books.actions'; // <== added DeleteBook==> +import { BookService } from '../../app/shared/services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { BookDto } from '../../app/shared/models'; + +export class BooksStateModel { + public book: PagedResultDto; +} -@State({ +@State({ name: 'BooksState', - defaults: { books: {} } as Books.State, + defaults: { book: {} } as BooksStateModel, }) @Injectable() export class BooksState { @Selector() - static getBooks(state: Books.State) { - return state.books.items || []; + static getBooks(state: BooksStateModel) { + return state.book.items || []; } - constructor(private booksService: BooksService) { } + constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { - return this.booksService.get().pipe( - tap(booksResponse => { + get(ctx: StateContext) { + return this.bookService.getListByInput().pipe( + tap((booksResponse) => { ctx.patchState({ - books: booksResponse, + book: booksResponse, }); - }), + }) ); } @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { + save(ctx: StateContext, action: CreateUpdateBook) { if (action.id) { - return this.booksService.update(action.payload, action.id); + return this.bookService.updateByIdAndInput(action.payload, action.id); } else { - return this.booksService.create(action.payload); + return this.bookService.createByInput(action.payload); } } - //<== added DeleteBook ==> + // <== added DeleteBook action listener ==> @Action(DeleteBook) - delete(ctx: StateContext, action: DeleteBook) { - return this.booksService.delete(action.id); + delete(ctx: StateContext, action: DeleteBook) { + return this.bookService.deleteById(action.id); } } ``` @@ -1379,27 +1282,8 @@ export class BooksState { - We added `DeleteBook` action listener to the end of the file. - - -#### Add a delete button - - -Open `book-list.component.html` in `app\books\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below: - -```html -
- - -
-``` - -The final actions dropdown UI looks like below: -![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) - -#### Delete confirmation dialog +#### Delete confirmation popup Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService`. @@ -1410,26 +1294,29 @@ import { ConfirmationService } from '@abp/ng.theme.shared'; //... constructor( - private store: Store, private fb: FormBuilder, - private booksService: BooksService, - private confirmationService: ConfirmationService // <== added this line ==> + private store: Store, + private fb: FormBuilder, + private bookService: BookService, + private confirmation: ConfirmationService // <== added this line ==> ) { } ``` * We imported `ConfirmationService`. * We injected `ConfirmationService` to the constructor. +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 : ```js -import { GetBooks, CreateUpdateBook, DeleteBook } from '../../store/actions'; //<== added DeleteBook ==> +import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/books.actions' ;// <== imported DeleteBook ==> -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== added Confirmation ==> +import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==> //... -delete(id: string, name: string) { - this.confirmationService +delete(id: string) { + this.confirmation .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') .subscribe(status => { if (status === Confirmation.Status.confirm) { @@ -1439,10 +1326,30 @@ delete(id: string, name: string) { } ``` + 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: ![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) + +#### Add a delete button + + +Open `book-list.component.html` in `app\books\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below: + +```html +
+ + +
+``` + +The final actions dropdown UI looks like below: + +![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) + {{end}} ### Next Part diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md index 4cbefde69e..14822c9fe2 100644 --- a/docs/en/Tutorials/Part-3.md +++ b/docs/en/Tutorials/Part-3.md @@ -96,9 +96,9 @@ namespace Acme.BookStore ```` * `IRepository` is injected and used it in the `SeedAsync` to create two book entities as the test data. +* `IGuidGenerator` is injected to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases. Further information, see the [Guid generation document](../Guid-Generation.md). -### Testing the application service BookAppService -* `IGuidGenerator` is injected to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases. Further information, see the [Guid generation document](https://docs.abp.io/{{Document_Language_Code}}/abp/{{Document_Version}}/Guid-Generation). +### Testing the application service BookAppService Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: diff --git a/docs/en/Tutorials/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/images/bookstore-angular-file-tree.png index 28e570f604..a3197b6457 100644 Binary files a/docs/en/Tutorials/images/bookstore-angular-file-tree.png and b/docs/en/Tutorials/images/bookstore-angular-file-tree.png differ diff --git a/docs/en/Tutorials/images/generate-proxy-command.png b/docs/en/Tutorials/images/generate-proxy-command.png new file mode 100644 index 0000000000..f850ce07a2 Binary files /dev/null and b/docs/en/Tutorials/images/generate-proxy-command.png differ diff --git a/docs/en/Tutorials/images/generated-proxies.png b/docs/en/Tutorials/images/generated-proxies.png new file mode 100644 index 0000000000..9e466e7d55 Binary files /dev/null and b/docs/en/Tutorials/images/generated-proxies.png differ diff --git a/docs/en/UI/Angular/Config-State.md b/docs/en/UI/Angular/Config-State.md index 7fc202982c..71a5da01f0 100644 --- a/docs/en/UI/Angular/Config-State.md +++ b/docs/en/UI/Angular/Config-State.md @@ -236,7 +236,7 @@ const newRoute: ABP.Route = { path: "page", invisible: false, order: 2, - requiredPolicy: "MyProjectName::MyNewPage" + requiredPolicy: "MyProjectName.MyNewPage" }; this.config.dispatchAddRoute(newRoute); @@ -248,23 +248,25 @@ The `newRoute` will be placed as at root level, i.e. without any parent routes a If you want **to add a child route, you can do this:** ```js +import { eIdentityRouteNames } from '@abp/ng.identity'; + // this.config is instance of ConfigStateService const newRoute: ABP.Route = { - parentName: "AbpAccount::Login", + parentName: eIdentityRouteNames.IdentityManagement, name: "My New Page", iconClass: "fa fa-dashboard", path: "page", invisible: false, order: 2, - requiredPolicy: "MyProjectName::MyNewPage" + requiredPolicy: "MyProjectName.MyNewPage" }; 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 `'AbpAccount::Login'` and its url will be set as `'/account/login/page'`. +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'`. #### Route Configuration Properties @@ -291,4 +293,4 @@ Please refer to `Config.Environment` type for all the properties you can pass to ## What's Next? -* [Component Replacement](./Component-Replacement.md) \ No newline at end of file +- [Modifying the Menu](./Modifying-the-Menu.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Confirmation-Service.md b/docs/en/UI/Angular/Confirmation-Service.md new file mode 100644 index 0000000000..2d1c5c1843 --- /dev/null +++ b/docs/en/UI/Angular/Confirmation-Service.md @@ -0,0 +1,185 @@ +# Confirmation Popup + +You can use the `ConfirmationService` in @abp/ng.theme.shared package to display a confirmation popup by placing at the root level in your project. + + +## Getting Started + +You do not have to provide the `ConfirmationService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + + +```js +import { ConfirmationService } from '@abp/ng.theme.shared'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private confirmation: ConfirmationService) {} +} +``` + +## Usage + +You can use the `success`, `warn`, `error`, and `info` methods of `ConfirmationService` to display a confirmation popup. + +### How to Display a Confirmation Popup + +```js +const confirmationStatus$ = this.confirmation.success('Message', 'Title'); +``` + +- The `ConfirmationService` methods accept three parameters that are `message`, `title`, and `options`. +- `success`, `warn`, `error`, and `info` methods return an [RxJS Subject](https://rxjs-dev.firebaseapp.com/guide/subject) to listen to confirmation popup closing event. The type of event value is [`Confirmation.Status`](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts#L24) that is an enum. + +### How to Listen Closing Event + +You can subscribe to the confirmation closing event like below: + +```js +import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; + +constructor(private confirmation: ConfirmationService) {} + +this.confirmation + .warn('::WillBeDeleted', { key: '::AreYouSure', defaultValue: 'Are you sure?' }) + .subscribe((status: Confirmation.Status) => { + // your code here + }); +``` + + +- The `message` and `title` parameters accept a string, localization key or localization object. See the [localization document](./Localization.md) +- `Confirmation.Status` is an enum and has three properties; + - `Confirmation.Status.confirm` is a closing event value that will be emitted when the popup is closed by the confirm button. + - `Confirmation.Status.reject` is a closing event value that will be emitted when the popup is closed by the cancel button. + - `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape. + + +If you are not interested in the confirmation status, you do not have to subscribe to the returned observable: + +```js +this.confirmation.error('You are not authorized.', 'Error'); +``` + +### How to Display a Confirmation Popup With Given Options + +Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods: + +```js +const options: Partial = { + hideCancelBtn: false, + hideYesBtn: false, + cancelText: 'Close', + yesText: 'Confirm', + messageLocalizationParams: ['Demo'], + titleLocalizationParams: [], +}; + +this.confirmation.warn( + 'AbpIdentity::RoleDeletionConfirmationMessage', + 'Are you sure?', + options, +); +``` + +- `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false` +- `hideYesBtn` option hides the confirmation button when `true`. Default value is `false` +- `cancelText` is the text of the cancellation button. A localization key or localization object can be passed. Default value is `AbpUi::Cancel` +- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes` +- `messageLocalizationParams` is the interpolation parameters for the localization of the message. +- `titleLocalizationParams` is the interpolation parameters for the localization of the title. + +With the options above, the confirmation popup looks like this: + +![confirmation](./images/confirmation.png) + +You are able to pass in an HTML string as title, message, or button texts. Here is an example: + +```js +const options: Partial = { + yesText: 'Yes, delete it', +}; + +this.confirmation.warn( + ` + Role Demo will be deleted +
+ Do you confirm that? + `, + 'Are you sure?', + options, +); +``` + +Since the values are HTML now, localization should be handled manually. Check out the [LocalizationService](./Localization#using-the-localization-service) to see how you can accomplish that. + +> Please note that all strings will be sanitized by Angular and not every HTML string will work. Only values that are considered as "safe" by Angular will be displayed. + +### How to Remove a Confirmation Popup + +The open confirmation popup can be removed manually via the `clear` method: + +```js +this.confirmation.clear(); +``` + +## API + +### success + +```js +success( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Confirmation` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts) + + +### warn + +```js +warn( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### error + +```js +error( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### info + +```js +info( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### clear + +```js +clear( + status: Confirmation.Status = Confirmation.Status.dismiss +): void +``` + +- `status` parameter is the value of the confirmation closing event. + + +## What's Next? + +- [Toast Overlay](./Toaster-Service.md) diff --git a/docs/en/UI/Angular/Http-Requests.md b/docs/en/UI/Angular/Http-Requests.md index 71878c8045..2ac5aa1b49 100644 --- a/docs/en/UI/Angular/Http-Requests.md +++ b/docs/en/UI/Angular/Http-Requests.md @@ -206,4 +206,4 @@ You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob ## What's Next? -* [Localization](./Localization.md) \ No newline at end of file +* [Localization](./Localization.md) diff --git a/docs/en/UI/Angular/Modifying-the-Menu.md b/docs/en/UI/Angular/Modifying-the-Menu.md new file mode 100644 index 0000000000..450a5aecd1 --- /dev/null +++ b/docs/en/UI/Angular/Modifying-the-Menu.md @@ -0,0 +1,199 @@ +# Modifying the Menu + +The menu is inside the `ApplicationLayoutComponent` in the @abp/ng.theme.basic package. There are several methods for modifying the menu elements. This document covers these methods. If you would like to replace the menu completely, please refer to [Component Replacement documentation](./Component-Replacement.md) and learn how to replace a layout. + + + + + +## How to Add a Logo + +The `logoUrl` property in the environment variables is the url of the logo. + +You can add your logo to `src/assets` folder and set the `logoUrl` as shown below: + +```js +export const environment = { + // other configurations + application: { + name: 'MyProjectName', + logoUrl: 'assets/logo.png', + }, + // other configurations +}; +``` + +## How to Add a Navigation Element + +### 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 add the `routes` property like below: + +```js +{ + path: 'your-path', + data: { + routes: { + name: 'Your navigation', + order: 3, + iconClass: 'fas fa-question-circle', + requiredPolicy: 'permission key here', + children: [ + { + path: 'child', + name: 'Your child navigation', + order: 1, + requiredPolicy: 'permission key here', + }, + ], + } as ABP.Route, // can be imported from @abp/ng.core + } +} +``` + +- `name` is the label of the navigation element. A localization key or a localization object can be passed. +- `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 +// 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; + +this.config.dispatchAddRoute(newRoute); +// returns a state stream which emits after dispatch action is complete +``` + +The `newRoute` will be placed as at root level, i.e. without any parent routes, and its url will be stored as `'/path'`. + +If you want **to add a child route, you can do this:** + +```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; + +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 new route will be added like below: + +![navigation-menu-via-config-state](./images/navigation-menu-via-config-state.png) + +## How to Patch 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. + +```js +// this.config is instance of ConfigStateService +// eIdentityRouteNames enum can be imported from @abp/ng.identity + +const newRouteConfig: Partial = { + iconClass: 'fas fa-home', + parentName: eIdentityRouteNames.Administration, + order: 0, + children: [ + { + name: 'Dashboard', + path: 'dashboard', + }, + ], +}; + +this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig); +// returns a state stream which emits after dispatch action is complete +``` + +* Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`. +* Added an icon. +* Specified the order. +* Added a child route named _Dashboard_. + +After the patch above, navigation elements looks like below: + +![navigation-menu-after-patching](./images/navigation-menu-after-patching.png) + + +## How to Add an Element to Right Part of the Menu + +The right part elements are stored in the `LayoutState` that is in the @abp/ng.theme.basic package. + +The `dispatchAddNavigationElement` method of the `LayoutStateService` adds an element to the right part of the menu. + +You can insert an element by adding your template to `app.component` and calling the `dispatchAddNavigationElement` method: + +```js +import { Layout, LayoutStateService } from '@abp/ng.theme.basic'; // added this line + +@Component({ + selector: 'app-root', + template: ` + + + + `, +}) +export class AppComponent { + // Added ViewChild + @ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef; + + constructor(private layout: LayoutStateService) {} // injected LayoutStateService + + // Added ngAfterViewInit + ngAfterViewInit() { + const newElement = { + name: 'Search', + element: this.searchElementRef, + order: 1, + } as Layout.NavigationElement; + + this.layout.dispatchAddNavigationElement(newElement); + } +} +``` + +This inserts a search input to the menu. The final UI looks like below: + +![navigation-menu-search-input](./images/navigation-menu-search-input.png) + +## How to Remove an Element From Right Part of the Menu + +TODO + + +## What's Next + +* [Component Replacement](./Component-Replacement.md) diff --git a/docs/en/UI/Angular/Permission-Management.md b/docs/en/UI/Angular/Permission-Management.md index ababd25e7f..d86e8c96b2 100644 --- a/docs/en/UI/Angular/Permission-Management.md +++ b/docs/en/UI/Angular/Permission-Management.md @@ -76,4 +76,4 @@ Granted Policies are stored in the `auth` property of `ConfigState`. ## What's Next? -* [Config State](./Config-State.md) \ No newline at end of file +- [Confirmation Popup](./Confirmation-Service.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Toaster-Service.md b/docs/en/UI/Angular/Toaster-Service.md new file mode 100644 index 0000000000..b194811d9a --- /dev/null +++ b/docs/en/UI/Angular/Toaster-Service.md @@ -0,0 +1,158 @@ +# Toast Overlay + +You can use the `ToasterService` in @abp/ng.theme.shared package to display messages in an overlay by placing at the root level in your project. + + +## Getting Started + +You do not have to provide the `ToasterService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + + +```js +import { ToasterService } from '@abp/ng.theme.shared'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private toaster: ToasterService) {} +} +``` + +## Usage + +You can use the `success`, `warn`, `error`, and `info` methods of `ToasterService` to display an overlay. + +### How to Display a Toast Overlay + +```js +this.toast.success('Message', 'Title'); +``` + +- The `ToasterService` methods accept three parameters that are `message`, `title`, and `options`. +- `success`, `warn`, `error`, and `info` methods return the id of opened toast overlay. The toast can be removed with this id. + +### How to Display a Toast Overlay With Given Options + +Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods: + +```js +import { Toaster, ToasterService } from '@abp/ng.theme.shared'; +//... + +constructor(private toaster: ToasterService) {} + +//... +const options: Partial = { + life: 10000, + sticky: false, + closable: true, + tapToDismiss: true, + messageLocalizationParams: ['Demo', '1'], + titleLocalizationParams: [] + }; + + this.toaster.error('AbpUi::EntityNotFoundErrorMessage', 'AbpUi::Error', options); +``` + +- `life` option is the closing time in milliseconds. Default value is `5000`. +- `sticky` option keeps toast overlay on the screen by ignoring the `life` option when `true`. Default value is `false`. +- `closable` option displays the close icon on the toast overlay when it is `true`. Default value is `true`. +- `tapToDismiss` option, when `true`, allows closing the toast overlay by clicking over it. Default value is `false`. +- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`. +- `messageLocalizationParams` is the interpolation parameters for the localization of the message. +- `titleLocalizationParams` is the interpolation parameters for the localization of the title. + +With the options above, the toast overlay looks like this: + +![toast](./images/toast.png) + +### How to Remove a Toast Overlay + +The open toast overlay can be removed manually via the `remove` method by passing the `id` of toast: + +```js +const toastId = this.toast.success('Message', 'Title') + +this.toast.remove(toastId); +``` + +### How to Remove All Toasts + +The all open toasts can be removed manually via the `clear` method: + +```js +this.toast.clear(); +``` + +## API + +### success + +```js +success( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +- `Config` namespace can be imported from `@abp/ng.core`. +- `Toaster` namespace can be imported from `@abp/ng.theme.shared`. + +> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Toaster` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts) + + +### warn + +```js +warn( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +### error + +```js +error( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +### info + +```js +info( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): number +``` + +### remove + +```js +remove(id: number): void +``` + +Removes an open toast by the given id. + +### clear + +```js +clear(): void +``` + +Removes all open toasts. + +## See Also + +- [Confirmation Popup](./Confirmation-Service.md) + +## What's Next? + +- [Config State](./Config-State.md) diff --git a/docs/en/UI/Angular/images/confirmation.png b/docs/en/UI/Angular/images/confirmation.png new file mode 100644 index 0000000000..efe4c98ea7 Binary files /dev/null and b/docs/en/UI/Angular/images/confirmation.png differ diff --git a/docs/en/UI/Angular/images/navigation-menu-after-patching.png b/docs/en/UI/Angular/images/navigation-menu-after-patching.png new file mode 100644 index 0000000000..2f6bf08c88 Binary files /dev/null and b/docs/en/UI/Angular/images/navigation-menu-after-patching.png differ diff --git a/docs/en/UI/Angular/images/navigation-menu-search-input.png b/docs/en/UI/Angular/images/navigation-menu-search-input.png new file mode 100644 index 0000000000..ebdc05e3e0 Binary files /dev/null and b/docs/en/UI/Angular/images/navigation-menu-search-input.png differ diff --git a/docs/en/UI/Angular/images/navigation-menu-via-app-routing.png b/docs/en/UI/Angular/images/navigation-menu-via-app-routing.png new file mode 100644 index 0000000000..4d5f61301c Binary files /dev/null and b/docs/en/UI/Angular/images/navigation-menu-via-app-routing.png differ diff --git a/docs/en/UI/Angular/images/navigation-menu-via-config-state.png b/docs/en/UI/Angular/images/navigation-menu-via-config-state.png new file mode 100644 index 0000000000..19944f154d Binary files /dev/null and b/docs/en/UI/Angular/images/navigation-menu-via-config-state.png differ diff --git a/docs/en/UI/Angular/images/toast.png b/docs/en/UI/Angular/images/toast.png new file mode 100644 index 0000000000..24cdd0fe0c Binary files /dev/null and b/docs/en/UI/Angular/images/toast.png differ diff --git a/docs/en/UI/Common/Utils/Linked-List.md b/docs/en/UI/Common/Utils/Linked-List.md index 0d6b259603..0c047d28e5 100644 --- a/docs/en/UI/Common/Utils/Linked-List.md +++ b/docs/en/UI/Common/Utils/Linked-List.md @@ -1270,7 +1270,7 @@ find(predicate: ListIteratorFn): ListNode | undefined Finds the first node from the list that matches the given predicate: ```js -list.addTailMany(['a', 'b', 'b', 'c']); +list.addManyTail(['a', 'b', 'b', 'c']); // "a" <-> "b" <-> "b" <-> "c" @@ -1294,7 +1294,7 @@ findIndex(predicate: ListIteratorFn): number Finds the position of the first node from the list that matches the given predicate: ```js -list.addTailMany(['a', 'b', 'b', 'c']); +list.addManyTail(['a', 'b', 'b', 'c']); // "a" <-> "b" <-> "b" <-> "c" @@ -1322,7 +1322,7 @@ get(position: number): ListNode | undefined Finds and returns the node with specific position in the list: ```js -list.addTailMany(['a', 'b', 'c']); +list.addManyTail(['a', 'b', 'c']); // "a" <-> "b" <-> "c" @@ -1346,7 +1346,7 @@ indexOf(value: T, compareFn?: ListComparisonFn): number Finds the position of the first node from the list that has the given value: ```js -list.addTailMany(['a', 'b', 'b', 'c']); +list.addManyTail(['a', 'b', 'b', 'c']); // "a" <-> "b" <-> "b" <-> "c" @@ -1368,7 +1368,7 @@ i3 === -1 You may pass a custom compare function to detect the searched value: ```js -list.addTailMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]); +list.addManyTail([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]); // {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} @@ -1408,7 +1408,7 @@ forEach(iteratorFn: ListIteratorFn): void Runs a function on all nodes in a linked list from head to tail: ```js -list.addTailMany(['a', 'b', 'c']); +list.addManyTail(['a', 'b', 'c']); // "a" <-> "b" <-> "c" @@ -1425,7 +1425,7 @@ list.forEach((node, index) => console.log(node.value + index)); A linked list is iterable. In other words, you may use methods like `for...of` on it. ```js -list.addTailMany(['a', 'b', 'c']); +list.addManyTail(['a', 'b', 'c']); // "a" <-> "b" <-> "c" @@ -1449,7 +1449,7 @@ toArray(): T[] Converts a linked list to an array of values: ```js -list.addTailMany(['a', 'b', 'c']); +list.addManyTail(['a', 'b', 'c']); // "a" <-> "b" <-> "c" @@ -1471,7 +1471,7 @@ toNodeArray(): ListNode[] Converts a linked list to an array of nodes: ```js -list.addTailMany(['a', 'b', 'c']); +list.addManyTail(['a', 'b', 'c']); // "a" <-> "b" <-> "c" @@ -1495,7 +1495,7 @@ toString(mapperFn: ListMapperFn = JSON.stringify): string Converts a linked list to a string representation of nodes and their relations: ```js -list.addTailMany(['a', 2, 'c', { k: 4, v: 'd' }]); +list.addManyTail(['a', 2, 'c', { k: 4, v: 'd' }]); // "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"} @@ -1562,7 +1562,7 @@ export class ListNode { - `previous` refers to the previous node in the list. ```js -list.addTailMany([ 0, 1, 2 ]); +list.addManyTail([ 0, 1, 2 ]); console.log( list.head.value, // 0 diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 8b5d41aa59..c0515c18de 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -221,8 +221,11 @@ }, { "text": "Domain Driven Design", - "path": "Domain-Driven-Design.md", "items": [ + { + "text": "Overall", + "path": "Domain-Driven-Design.md" + }, { "text": "Domain Layer", "items": [ @@ -337,10 +340,22 @@ "text": "Permission Management", "path": "UI/Angular/Permission-Management.md" }, + { + "text": "Confirmation Popup", + "path": "UI/Angular/Confirmation-Service.md" + }, + { + "text": "Toast Overlay", + "path": "UI/Angular/Toaster-Service.md" + }, { "text": "Config State", "path": "UI/Angular/Config-State.md" }, + { + "text": "Modifying the Menu", + "path": "UI/Angular/Modifying-the-Menu.md" + }, { "text": "Component Replacement", "path": "UI/Angular/Component-Replacement.md" @@ -385,8 +400,11 @@ }, { "text": "Data Access", - "path": "Data-Access.md", "items": [ + { + "text": "Overall", + "path": "Data-Access.md" + }, { "text": "Connection Strings", "path": "Connection-Strings.md" @@ -469,8 +487,11 @@ }, { "text": "Startup Templates", - "path": "Startup-Templates/Index.md", "items": [ + { + "text": "Overall", + "path": "Startup-Templates/Index.md" + }, { "text": "Application", "path": "Startup-Templates/Application.md" diff --git a/docs/zh-Hans/API/Auto-API-Controllers.md b/docs/zh-Hans/API/Auto-API-Controllers.md index 93ac124913..c3f5d44d71 100644 --- a/docs/zh-Hans/API/Auto-API-Controllers.md +++ b/docs/zh-Hans/API/Auto-API-Controllers.md @@ -6,7 +6,7 @@ ABP可以按照惯例 **自动** 将你的应用程序服务配置为API控制 ## 配置 -基本配置很简单. 只需配置`AbpAspNetCoreMvcOptions`并使用`ConventionalControllers.Create`方法,如下所示: +基本配置很简单. 只需配置`AbpAspNetCoreMvcOptions`并使用`ConventionalControllers.Create`方法,如下所示: ````csharp [DependsOn(BookStoreApplicationModule)] @@ -82,7 +82,7 @@ Configure(options => * 删除'**Async**'后缀. 如果方法名称为'GetPhonesAsync',则变为`GetPhones`. * 删除**HTTP method前缀**. 基于的HTTP method删除`GetList`,`GetAll`,`Get`,`Put`,`Update`,`Delete`,`Remove`,`Create`,`Add`,`Insert`,`Post`和`Patch`前缀, 因此`GetPhones`变为`Phones`, 因为`Get`前缀和GET请求重复. * 将结果转换为**camelCase**. - * 如果生成的操作名称为**空**,则它不会添加到路径中.否则它会被添加到路由中(例如'/phones').对于`GetAllAsync`方法名称,它将为空,因为`GetPhonesAsync`方法名称将为`phone`. + * 如果生成的操作名称为**空**,则它不会添加到路径中.否则它会被添加到路由中(例如'/phones').对于`GetAllAsync`方法名称,它将为空,因为`GetPhonesAsync`方法名称将为`phone`. * 可以通过设置`UrlActionNameNormalizer`选项来自定义.It's an action delegate that is called for every method. * 如果有另一个带有'Id'后缀的参数,那么它也会作为最终路线段添加到路线中(例如'/phoneId'). diff --git a/docs/zh-Hans/Application-Services.md b/docs/zh-Hans/Application-Services.md index 72e0319cde..d0e29c2881 100644 --- a/docs/zh-Hans/Application-Services.md +++ b/docs/zh-Hans/Application-Services.md @@ -380,4 +380,4 @@ public class DistrictKey ### 生命周期 -应用服务的生命周期是[transient](Dependency-Injection)的,它们会自动注册到依赖注入系统. \ No newline at end of file +应用服务的生命周期是[transient](Dependency-Injection)的,它们会自动注册到依赖注入系统. \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md index 848d043312..ebfeb6f2aa 100644 --- a/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md +++ b/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md @@ -86,7 +86,7 @@ ABP框架定义了Tag Helper用于简单的创建bootstrap按钮. ### `icon-type` -`icon-type` 是一个可选参数。它的默认值是 `FontAwesome`. 你可以创建自己的图标类型提供程序并更改它. +`icon-type` 是一个可选参数.它的默认值是 `FontAwesome`. 你可以创建自己的图标类型提供程序并更改它. 你可以为按钮选择以下图标类型: diff --git a/docs/zh-Hans/Audit-Logging.md b/docs/zh-Hans/Audit-Logging.md index a028e640b2..a8b6e66449 100644 --- a/docs/zh-Hans/Audit-Logging.md +++ b/docs/zh-Hans/Audit-Logging.md @@ -43,12 +43,12 @@ Configure(options => * `IsEnabledForGetRequests` (默认值: `false`): HTTP GET请求通常不应该在数据库进行任何更改,审计日志系统不会为GET请求保存审计日志对象. 将此值设置为 `true` 可为GET请求启用审计日志系统. * `ApplicationName`: 如果有多个应用程序保存审计日志到单一的数据库,使用此属性设置为你的应用程序名称区分不同的应用程序日志. * `IgnoredTypes`: 审计日志系统忽略的 `Type` 列表. 如果它是实体类型,则不会保存此类型实体的更改. 在序列化操作参数时也使用此列表. -* `EntityHistorySelectors`:选择器列表,用于确定是否选择了用于保存实体更改的实体类型. 有关详细信息请参阅下面的部分. +* `EntityHistorySelectors`:选择器列表,用于确定是否选择了用于保存实体更改的实体类型. 有关详细信息请参阅下面的部分. * `Contributors`: `AuditLogContributor` 实现的列表. 贡献者是扩展审计日志系统的一种方式. 有关详细信息请参阅下面的"审计日志贡献者"部分. ### 实体历史选择器 -保存您的所有实体的所有变化将需要大量的数据库空间. 出于这个原因**审计日志系统不保存为实体的任何改变,除非你明确地对其进行配置**. +保存你的所有实体的所有变化将需要大量的数据库空间. 出于这个原因**审计日志系统不保存为实体的任何改变,除非你明确地对其进行配置**. 要保存的所有实体的所有更改,只需使用 `AddAllEntities()` 扩展方法. @@ -131,7 +131,7 @@ public class HomeController : AbpController 可以为任何类型的类(注册到[依赖注入](Dependency-Injection.md)并从依赖注入解析)启用审计日志,默认情况下仅对控制器和应用程序服务启用. -对于任何需要被审计记录的类或方法都可以使用 `[Audited]` 和`IAuditingEnabled`.此外,您的类可以(直接或固有的)实现 `IAuditingEnabled` 接口以认启用该类的审计日志记录. +对于任何需要被审计记录的类或方法都可以使用 `[Audited]` 和`IAuditingEnabled`.此外,你的类可以(直接或固有的)实现 `IAuditingEnabled` 接口以认启用该类的审计日志记录. ### 启用/禁用 实体 & 属性 @@ -211,8 +211,8 @@ public class MyUser : Entity * **AuditLogInfo**: 具有以下属性: * `ApplicationName`: 当你保存不同的应用审计日志到同一个数据库,这个属性用来区分应用程序. - * `UserId`:当前用户的Id,用户未登录为 `null`. - * `UserName`:当前用户的用户名,如果用户已经登录(这里的值不依赖于标识模块/系统进行查找). + * `UserId`:当前用户的Id,用户未登录为 `null`. + * `UserName`:当前用户的用户名,如果用户已经登录(这里的值不依赖于标识模块/系统进行查找). * `TenantId`: 当前租户的Id,对于多租户应用. * `TenantName`: 当前租户的名称,对于多租户应用. * `ExecutionTime`: 审计日志对象创建的时间. @@ -222,28 +222,28 @@ public class MyUser : Entity * `ClientIpAddress`: 客户端/用户设备的IP地址. * `CorrelationId`: 当前[相关Id](CorrelationId.md). 相关Id用于在单个逻辑操作中关联由不同应用程序(或微服务)写入的审计日志. * `BrowserInfo`: 当前用户的浏览器名称/版本信息,如果有的话. - * `HttpMethod`: 当前HTTP请求的方法(GET,POST,PUT,DELETE ...等). + * `HttpMethod`: 当前HTTP请求的方法(GET,POST,PUT,DELETE ...等). * `HttpStatusCode`: HTTP响应状态码. * `Url`: 请求的URL. * **AuditLogActionInfo**: 一个 审计日志动作通常是web请求期间控制器动作或[应用服务](Application-Services.md)方法调用. 一个审计日志可以包含多个动作. 动作对象具有以下属性: - * `ServiceName`:执行的控制器/服务的名称. - * `MethodName`:控制器/服务执行的方法的名称. - * `Parameters`:传递给方法的参数的JSON格文本. + * `ServiceName`:执行的控制器/服务的名称. + * `MethodName`:控制器/服务执行的方法的名称. + * `Parameters`:传递给方法的参数的JSON格文本. * `ExecutionTime`: 执行的时间. * `ExecutionDuration`: 方法执行时长,以毫秒为单位. 可以用来观察方法的性能. * **EntityChangeInfo**: 表示一个实体在Web请求中的变更. 审计日志可以包含0个或多个实体的变更. 实体变更具有以下属性: * `ChangeTime`: 当实体被改变的时间. - * `ChangeType`:具有以下字段的枚举: `Created`(0), `Updated`(1)和 `Deleted`(2). + * `ChangeType`:具有以下字段的枚举: `Created`(0), `Updated`(1)和 `Deleted`(2). * `EntityId`: 更改实体的Id. - * `EntityTenantId`:实体所属的租户Id. + * `EntityTenantId`:实体所属的租户Id. * `EntityTypeFullName`: 实体的类型(类)的完整命名空间名称(例如Book实体的*Acme.BookStore.Book*. * **EntityPropertyChangeInfo**: 表示一个实体的属性的更改.一个实体的更改信息(上面已说明)可含有具有以下属性的一个或多个属性的更改: * `NewValue`: 属性的新值. 如果实体已被删除为 `null`. - * `OriginalValue`:变更前旧/初始值. 如果实体是新创建为 `null`. + * `OriginalValue`:变更前旧/初始值. 如果实体是新创建为 `null`. * `PropertyName`: 实体类的属性名称. - * `PropertyTypeFullName`:属性类型的完整命名空间名称. + * `PropertyTypeFullName`:属性类型的完整命名空间名称. * **Exception**: 审计日志对象可能包含零个或多个异常. 可以得到失败请求的异常信息. -* **Comment**:用于将自定义消息添加到审计日志条目的任意字符串值. 审计日志对象可能包含零个或多个注释. +* **Comment**:用于将自定义消息添加到审计日志条目的任意字符串值. 审计日志对象可能包含零个或多个注释. 除了上面说明的标准属性之外,`AuditLogInfo`, `AuditLogActionInfo` 和 `EntityChangeInfo` 对象还实现了`IHasExtraProperties` 接口,你可以向这些对象添加自定义属性. @@ -331,7 +331,7 @@ public class MyService : ITransientDependency ### 手动创建审计日志范围 你很少需要手动创建审计日志的范围,但如果你需要,可以使用 `IAuditingManager` 创建审计日志的范围. -例: +例: ````csharp public class MyService : ITransientDependency @@ -366,7 +366,7 @@ public class MyService : ITransientDependency } ```` -您可以调用其他服务,它们可能调用其他服务,它们可能更改实体,等等. 所有这些交互都保存为finally块中的一个审计日志对象. +你可以调用其他服务,它们可能调用其他服务,它们可能更改实体,等等. 所有这些交互都保存为finally块中的一个审计日志对象. ## 审计日志模块 diff --git a/docs/zh-Hans/Authorization.md b/docs/zh-Hans/Authorization.md index a43565279e..99139be3ac 100644 --- a/docs/zh-Hans/Authorization.md +++ b/docs/zh-Hans/Authorization.md @@ -153,7 +153,7 @@ myGroup.AddPermission( myGroup.AddPermission("Author_Management", isEnabled: false); ```` -通常你不需要定义禁用权限(除非您暂时想要禁用应用程序的功能). 无论怎样,你可能想要禁用依赖模块中定义的权限,这样你可以禁用相关的功能. 参阅下面的 "*更改依赖模块的权限定义*" 节,查看示例用法. +通常你不需要定义禁用权限(除非你暂时想要禁用应用程序的功能). 无论怎样,你可能想要禁用依赖模块中定义的权限,这样你可以禁用相关的功能. 参阅下面的 "*更改依赖模块的权限定义*" 节,查看示例用法. > 注意:检查一个未定义的权限会抛出异常,而被禁用的权限的返回禁止(false). diff --git a/docs/zh-Hans/Background-Workers.md b/docs/zh-Hans/Background-Workers.md index dfd26c7b82..1daedde384 100644 --- a/docs/zh-Hans/Background-Workers.md +++ b/docs/zh-Hans/Background-Workers.md @@ -2,7 +2,7 @@ ## 介绍 -背景工人在应用简单独立的线程在后台运行。一般来说,他们定期运行,以执行一些任务。例子; +背景工人在应用简单独立的线程在后台运行.一般来说,他们定期运行,以执行一些任务.例子; 后台工作者在应用程序后台运行的简单的独立线程,一般来说它们定期运行执行一些任务.例如; * 后台工作者可以定期**删除过时的日志**. @@ -72,7 +72,7 @@ public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase } ```` -* `AsyncPeriodicBackgroundWorkerBase` 使用 `AbpTimer`(线程安全定时器)对象来确定**时间段**. 我们可以在构造函数中设置了`Period` 属性。 +* `AsyncPeriodicBackgroundWorkerBase` 使用 `AbpTimer`(线程安全定时器)对象来确定**时间段**. 我们可以在构造函数中设置了`Period` 属性. * 它需要实现 `DoWorkAsync` 方法**执行**定期任务. * 最好使用 `PeriodicBackgroundWorkerContext` **解析依赖** 而不是构造函数. 因为 `AsyncPeriodicBackgroundWorkerBase` 使用 `IServiceScope` 在你的任务执行结束时会对其 **disposed**. * `AsyncPeriodicBackgroundWorkerBase` **捕获并记录** 由 `DoWorkAsync` 方法抛出的 **异常**. diff --git a/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md b/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md index 30a5d691e9..998a454abc 100644 --- a/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md @@ -13,7 +13,7 @@ ABP框架的主要目标之一是提供[创建微服务解决方案的便利基 - 使用[Ocelot](https://github.com/ThreeMammals/Ocelot)库开发了多个**网关** / BFF(后端为前端(Backend for Frontends)). - 使用[IdentityServer](https://identityserver.io/)框架开发**身份验证服务**.它也是一个带有必要UI的SSO(单点登录)应用程序. - 有**多个数据库**.一些微服务有自己的数据库,而一些服务/应用程序共享一个数据库(以演示不同的用例). -- 具有不同类型的数据库:**SQL Server**(使用**Entity Framework Core** ORM)和**MongoDB**. +- 具有不同类型的数据库:**SQL Server**(使用**Entity Framework Core** ORM)和**MongoDB**. - 有一个**控制台应用程序**来显示通过身份验证使用服务的最简单方法. - 使用[Redis](https://redis.io/)进行**分布式缓存**. - 使用[RabbitMQ](https://www.rabbitmq.com/)进行服务到服务(service-to-service)的**消息传递**. @@ -28,27 +28,27 @@ ABP框架的主要目标之一是提供[创建微服务解决方案的便利基 ## 路线图 -在第一个稳定版本(v1.0)之前还有很多工作要做.您可以在GitHub仓库上看到[优先的积压项目](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3ABacklog). +在第一个稳定版本(v1.0)之前还有很多工作要做.你可以在GitHub仓库上看到[优先的积压项目](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3ABacklog). 根据我们的估计,我们计划在2019年第二季度(可能在五月或六月)发布v1.0.所以,不用等待太长时间了.我们也对第一个稳定版本感到非常兴奋. 我们还将完善[文档](https://abp.io/documents/abp/latest),因为它现在还远未完成. -第一个版本可能不包含SPA模板.但是,如果可能的话,我们想要准备一个简单些的.SPA框架还没有确定下来.备选有:**Angular,React和Blazor**.请将您的想法写为对此帖的评论. +第一个版本可能不包含SPA模板.但是,如果可能的话,我们想要准备一个简单些的.SPA框架还没有确定下来.备选有:**Angular,React和Blazor**.请将你的想法写为对此帖的评论. ## 中文网 -中国有一个大型的ABP社区.他们创建了一个中文版的abp.io网站:https://abp.io/. 他们一直在保持更新.感谢中国的开发人员,特别是[Liming Ma](https://github.com/maliming). +中国有一个大型的ABP社区.他们创建了一个中文版的abp.io网站:https://abp.io/. 他们一直在保持更新.感谢中国的开发人员,特别是[Liming Ma](https://github.com/maliming). ## NDC {London} 2019 很高兴作为合作伙伴参加[NDC {London}](https://ndc-london.com/)2019 .我们已经与许多开发人员讨论过当前的ASP.NET Boilerplate和ABP vNext,我们得到了很好的反馈. -我们还有机会与[Scott Hanselman](https://twitter.com/shanselman)和[Jon Galloway](https://twitter.com/jongalloway)交谈.他们参观了我们的展位,我们谈到了ABP vNext的想法.他们喜欢新的ABP框架的功能,方法和目标.在twitter上查看一些照片和评论: +我们还有机会与[Scott Hanselman](https://twitter.com/shanselman)和[Jon Galloway](https://twitter.com/jongalloway)交谈.他们参观了我们的展位,我们谈到了ABP vNext的想法.他们喜欢新的ABP框架的功能,方法和目标.在twitter上查看一些照片和评论: ![scott-and-jon](scott-and-jon.png) ## 跟上步伐 -* 您可以标星并关注**GitHub**存储库:https://github.com/abpframework/abp -* 您可以关注官方**Twitter**帐户获取新闻:https://twitter.com/abpframework +* 你可以标星并关注**GitHub**存储库:https://github.com/abpframework/abp +* 你可以关注官方**Twitter**帐户获取新闻:https://twitter.com/abpframework diff --git a/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md index a25e1c0a97..6b3c76d2e2 100644 --- a/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md @@ -4,7 +4,7 @@ ABP v0.18已发布, 包含解决的[80+个issue](https://github.com/abpframework ## 网站更改 -[abp.io](https://abp.io)网站**完全更新**以突出ABP框架的目标和重要功能.文档和博客网址也会更改: +[abp.io](https://abp.io)网站**完全更新**以突出ABP框架的目标和重要功能.文档和博客网址也会更改: - `abp.io/documents`移至[docs.abp.io](https://docs.abp.io). - `abp.io/blog`转移到[blog.abp.io](https://blog.abp.io). @@ -21,25 +21,25 @@ ABP CLI现在是创建新项目的首选方式,你仍然可以从[开始](https: ### 用法 -使用命令行窗口安装ABP CLI: +使用命令行窗口安装ABP CLI: ```` bash dotnet tool install -g Volo.Abp.Cli ```` -创建一个新应用程序: +创建一个新应用程序: ```` bash abp new Acme.BookStore ```` -将模块添加到应用程序: +将模块添加到应用程序: ```` bash abp add-module Volo.Blogging ```` -更新解决方案中所有与ABP相关的包: +更新解决方案中所有与ABP相关的包: ```` bash abp update @@ -59,7 +59,7 @@ abp update ## 更改日志 -以下是此版本附带的一些其他功能和增强功能: +以下是此版本附带的一些其他功能和增强功能: * 新[Volo.Abp.Dapper](https://www.nuget.org/packages/Volo.Abp.Dapper)包. * 新[Volo.Abp.Specifications](https://www.nuget.org/packages/Volo.Abp.Specifications)包. diff --git a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md index 831cc791e9..9f82f6dce3 100644 --- a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md @@ -14,12 +14,12 @@ ABP v0.19已发布,包含解决的[~90个问题](https://github.com/abpframework * 更新了[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)和[下载页面](https://abp.io/get-started),以便能够使用新的UI选项生成项目. * 创建了[教程](https://docs.abp.io/en/abp/latest/Tutorials/Angular/Part-I)以使用新的UI选项快速入门. -我们基于最新的Angular工具和趋势创建了模板,文档和基础架构: +我们基于最新的Angular工具和趋势创建了模板,文档和基础架构: * 使用[NgBootstrap](https://ng-bootstrap.github.io/)和[PrimeNG](https://www.primefaces.org/primeng/)作为UI组件库.你可以使用自己喜欢的库,没问题,但预构建的模块可以使用这些库. * 使用[NGXS](https://ngxs.gitbook.io/ngxs/)作为状态管理库. -Angular是第一个SPA UI选项,但它不是最后一个.在v1.0发布之后,我们将开始第二个UI选项的工作.虽然尚未决定,但候选的有Blazor,React和Vue.js. 等待你的反馈.你可以使用以下issue进行投票(thumb): +Angular是第一个SPA UI选项,但它不是最后一个.在v1.0发布之后,我们将开始第二个UI选项的工作.虽然尚未决定,但候选的有Blazor,React和Vue.js. 等待你的反馈.你可以使用以下issue进行投票(thumb): * [Blazor](https://github.com/abpframework/abp/issues/394) * [Vue.js](https://github.com/abpframework/abp/issues/1168) diff --git a/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md index 7633f3ea33..c4d98244fb 100644 --- a/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md @@ -14,8 +14,8 @@ ABP框架越来越接近v1.0.我们打算在今年10月中旬发布1.0. 现在, ## Techorama荷兰2019 -[Techorama NL](https://techorama.nl/)是欧洲最大的会议之一.今年,Volosoft是会议的赞助商,并将有一个展位与软件开发人员讨论ABP框架和软件开发.我们的展位墙如下图所示: +[Techorama NL](https://techorama.nl/)是欧洲最大的会议之一.今年,Volosoft是会议的赞助商,并将有一个展位与软件开发人员讨论ABP框架和软件开发.我们的展位墙如下图所示: ![volosoft-booth](volosoft-booth.png) -如果您也参加会议,请到展位讨论ABP框架.我们还为您准备了一些私货:) +如果你也参加会议,请到展位讨论ABP框架.我们还为你准备了一些私货:) diff --git a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md index a17bef2e1b..73585582f5 100644 --- a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md @@ -24,7 +24,7 @@ 我们终于完成了**react native移动应用程序**.目前,它可以让你**登录**,管理**用户**和**租户**.它利用ABP框架相同的设置,授权和本地化系统. -应用程序的一些截图: +应用程序的一些截图: ![mobile-ui](react-native-ui.png) @@ -34,7 +34,7 @@ 从我们的Angular应用程序中调用服务器中的REST端点是很常见的.这种情况下,我们一般创建**服务**(在服务器上包含各个服务的方法)和**模型对象**(对应服务器上的[DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)). -除了手动创建这样的与服务器交互的服务外,我们可以使用像[NSWAG](https://github.com/RicoSuter/NSwag)工具来为我们生成服务代理.但是NSWAG有以下几个我们遇到的问题: +除了手动创建这样的与服务器交互的服务外,我们可以使用像[NSWAG](https://github.com/RicoSuter/NSwag)工具来为我们生成服务代理.但是NSWAG有以下几个我们遇到的问题: * 它产生一个**大,单一**的.ts文件; * 当你的应用程序增长时,它变得**太大**了. @@ -58,12 +58,12 @@ abp generate-proxy ### 添加模块的源代码 -应用程序启动模板带有一些[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index), 以**Nuget和NPM包**的方式**预先安装了** .这样做有几个重要的优点: +应用程序启动模板带有一些[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index), 以**Nuget和NPM包**的方式**预先安装了** .这样做有几个重要的优点: * 当新版本可用时, 你可以 **轻松地[升级](https://docs.abp.io/en/abp/latest/CLI#update)** 这些模块. * 你的解决方案**更干净**,这样你就可以专注于自己的代码. -但是,当你需要对一个依赖的模块**大量定制**时,就不如它的代码在你的应用程序中那么容易.为了解决这个问题,我们引入了一个[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)的新命令, 在你的解决方案中用代码**替换**Nuget包.用法很简单: +但是,当你需要对一个依赖的模块**大量定制**时,就不如它的代码在你的应用程序中那么容易.为了解决这个问题,我们引入了一个[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)的新命令, 在你的解决方案中用代码**替换**Nuget包.用法很简单: ````bash abp add-module --with-source-code @@ -75,19 +75,19 @@ abp add-module --with-source-code 此外,我们也创建了文档来说明如何定制依赖的模块而不改变它们的源代码(见下面的部分).仍然建议以包的方式使用模块,以便在以后可以轻松升级. -> 免费模块的源代码是**MIT**许可,所以你可以自由更改它们并添加到您的解决方案中. +> 免费模块的源代码是**MIT**许可,所以你可以自由更改它们并添加到你的解决方案中. ### 切换到预览版 ABP框架正在迅速发展,我们经常发布新版本.不过,如果你想更紧密地追随它,你可以使用**每日预览包**. -我们创建了一个ABP CLI命令来轻松地为你的解决方案**更新到最新的预览包**.在你的解决方案的根文件夹中运行以下命令: +我们创建了一个ABP CLI命令来轻松地为你的解决方案**更新到最新的预览包**.在你的解决方案的根文件夹中运行以下命令: ````bash abp switch-to-preview ```` -它会修改所有ABP相关的NuGet和NPM包的版本.当你需要时你也可以**切换回最新稳定版**: +它会修改所有ABP相关的NuGet和NPM包的版本.当你需要时你也可以**切换回最新稳定版**: ````bash abp switch-to-stable @@ -131,7 +131,7 @@ abp switch-to-stable ## 下一步? -我们未来几个月的目标如下: +我们未来几个月的目标如下: * 完成**文档和示例**,写更多的教程. * 使框架和现有模块的更加**可定制和可扩展**. diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md index 0a802ffdc1..658545c4fd 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -41,12 +41,12 @@ abp new Acme.BookStore * `--template` 或者 `-t`: 指定模板. 默认的模板是 `app`,会生成web项目.可用的模板有: * `app` (default): [应用程序模板](Startup-Templates/Application.md). 其他选项: - * `--ui` 或者 `-u`: 指定ui框架.默认`mvc`框架.其他选项: - * `mvc`: ASP.NET Core MVC.此模板的其他选项: + * `--ui` 或者 `-u`: 指定ui框架.默认`mvc`框架.其他选项: + * `mvc`: ASP.NET Core MVC.此模板的其他选项: * `--tiered`: 创建分层解决方案,Web和Http Api层在物理上是分开的.如果未指定会创建一个分层的解决方案,此解决方案没有那么复杂,适合大多数场景. - * `angular`: Angular. 这个模板还有一些额外的选项: + * `angular`: Angular. 这个模板还有一些额外的选项: * `--separate-identity-server`: 将Identity Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点. - * `none`: 无UI. 这个模板还有一些额外的选项: + * `none`: 无UI. 这个模板还有一些额外的选项: * `--separate-identity-server`: 将Identity Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点. * `--mobile` 或者 `-m`: 指定移动应用程序框架. 默认框架是 `react-native`. 其他选项: * `none`: 不包含移动应用程序. @@ -57,7 +57,7 @@ abp new Acme.BookStore * `module`: [Module template](Startup-Templates/Module.md). 其他选项: * `--no-ui`: 不包含UI.仅创建服务模块(也称为微服务 - 没有UI). * `--output-folder` 或者 `-o`: 指定输出文件夹,默认是当前目录. -* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,您会希望使用最新的版本. +* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,你会希望使用最新的版本. * `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D\localTemplate` 或 `https://.zip`). * `--create-solution-folder` 或者 `-csf`: 指定项目是在输出文件夹中的新文件夹中还是直接在输出文件夹中. * `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. 如果你不想使用默认,你可以设置自己的连接字符串. 默认的数据库提供程序是 `SQL Server`, 所以你只能输入SQL Server连接字符串! @@ -135,6 +135,8 @@ abp update [options] * `--include-previews` 或 `-p`: 将预览版, 测试版本 和 rc 包 同时更新到最新版本. * `--npm`: 仅更新NPM包 * `--nuget`: 仅更新的NuGet包 +* `--solution-path` 或 `-sp`: 指定解决方案路径/目录. 默认使用当前目录 +* `--solution-name` 或 `-sn`: 指定解决方案名称. 默认在目录中搜索`*.sln`文件. ### 切换到每晚构建(预览)包 @@ -164,6 +166,10 @@ CLI的一些功能需要登录到abp.io平台. 使用你的用户名登录 abp login ``` +```bash +abp login -p +``` + 请注意,新的登录将终止先前的会话并创建一个新的会话. ### logout @@ -186,9 +192,9 @@ abp generate-proxy [options] #### Options -* `--apiUrl` 或者 `-a`:指定HTTP API的根URL. 如果未指定这个选项,默认使用你Angular应用程序的`environment.ts`文件API URL. 在运行 `generate-proxy` 命令之前,你的host必须启动正在运行. +* `--apiUrl` 或者 `-a`:指定HTTP API的根URL. 如果未指定这个选项,默认使用你Angular应用程序的`environment.ts`文件API URL. 在运行 `generate-proxy` 命令之前,你的host必须启动正在运行. * `--ui` 或者 `-u`: 指定UI框架,默认框架是angular.当前只有angular一个选项, 但我们会通过更改CLI增加新的选项. 尽请关注! -* `--module` 或者 `-m`:指定模块名. 默认模块名称为app. 如果你想所有模块,你可以指定 `--module all` 命令. +* `--module` 或者 `-m`:指定模块名. 默认模块名称为app. 如果你想所有模块,你可以指定 `--module all` 命令. 示例: diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md index 1ba35adf80..caa448a14a 100644 --- a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md @@ -54,7 +54,7 @@ context.Services.Replace( ## 重写一个服务类 -大多数情况下,你会仅想改变服务当前实现的一个或几个方法. 重新实现完整的接口变的繁琐,更好的方法是继承原始类并重写方法。 +大多数情况下,你会仅想改变服务当前实现的一个或几个方法. 重新实现完整的接口变的繁琐,更好的方法是继承原始类并重写方法. ### 示例: 重写服务方法 diff --git a/docs/zh-Hans/Entities.md b/docs/zh-Hans/Entities.md index 1a059e477f..fd8528dc30 100644 --- a/docs/zh-Hans/Entities.md +++ b/docs/zh-Hans/Entities.md @@ -72,7 +72,7 @@ public class BookAppService : ApplicationService, IBookAppService * `BookAppService` 注入图书实体的默认[仓库](Repositories.md),使用`InsertAsync`方法插入 `Book` 到数据库中. * `GuidGenerator`类型是 `IGuidGenerator`,它是在`ApplicationService`基类中定义的属性. ABP将这样常用属性预注入,所以不需要手动[注入](Dependency-Injection.md). -* 如果您想遵循DDD最佳实践,请参阅下面的*聚合示例*部分. +* 如果你想遵循DDD最佳实践,请参阅下面的*聚合示例*部分. ### 具有复合键的实体 @@ -228,7 +228,7 @@ ABP框架不强制你应用任何DDD规则或模式.但是,当你准备应用的 ## 基类和接口的审计属性 -有一些属性,像`CreationTime`,`CreatorId`,`LastModificationTime`...在所有应用中都很常见. ABP框架提供了一些接口和基类来**标准化**这些属性,并**自动设置它们的值**. +有一些属性,像`CreationTime`,`CreatorId`,`LastModificationTime`...在所有应用中都很常见. ABP框架提供了一些接口和基类来**标准化**这些属性,并**自动设置它们的值**. ### 审计接口 @@ -285,7 +285,7 @@ ABP框架不强制你应用任何DDD规则或模式.但是,当你准备应用的 所有这些基类都有非泛型版本,可以使用 `AuditedEntity` 和 `FullAuditedAggregateRoot` 来支持复合主键; -所有这些基类也有 `... WithUser`,像 `FullAuditedAggregateRootWithUser` 和 `FullAuditedAggregateRootWithUser`. 这样就可以将导航属性添加到你的用户实体. 但在聚合根之间添加导航属性不是一个好做法,所以这种用法是不建议的(除非你使用EF Core之类的ORM可以很好地支持这种情况,并且你真的需要它. 请记住这种方法不适用于NoSQL数据库(如MongoDB),你必须真正实现聚合模式). +所有这些基类也有 `... WithUser`,像 `FullAuditedAggregateRootWithUser` 和 `FullAuditedAggregateRootWithUser`. 这样就可以将导航属性添加到你的用户实体. 但在聚合根之间添加导航属性不是一个好做法,所以这种用法是不建议的(除非你使用EF Core之类的ORM可以很好地支持这种情况,并且你真的需要它. 请记住这种方法不适用于NoSQL数据库(如MongoDB),你必须真正实现聚合模式). ## 额外的属性 diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md index caef6e76f2..caf5778753 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Migrations.md +++ b/docs/zh-Hans/Entity-Framework-Core-Migrations.md @@ -1,7 +1,7 @@  # EF Core数据库迁移 -本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论您可能希望为自己的应用程序实现的**各种场景**. +本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论你可能希望为自己的应用程序实现的**各种场景**. > 本文档适用于希望完全理解和自定义[应用程序启动模板](Startup-Templates/Application.md)附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循[启动教程](Tutorials/Index.md). @@ -95,7 +95,7 @@ Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; 这个项目有应用程序的 `DbContext`类(本例中的 `BookStoreDbContex` ). -**每个模块都使用自己的 `DbContext` 类**来访问数据库。同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体: +**每个模块都使用自己的 `DbContext` 类**来访问数据库.同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体: ````csharp [ConnectionStringName("Default")] @@ -268,10 +268,10 @@ public class BackgroundJobsDbContext ##### 重用模块的表 -您可能想在应用程序中**重用依赖模块的表**. 在这种情况下你有两个选择: +你可能想在应用程序中**重用依赖模块的表**. 在这种情况下你有两个选择: 1. 你可以**直接使用模块定义的实体**(你仍然可以在某种程度上[扩展实体](Customizing-Application-Modules-Extending-Entities.md)). -2. 你可以**创建一个新的实体**映射到同一个数据库表。 +2. 你可以**创建一个新的实体**映射到同一个数据库表. ###### 使用由模块定义的实体 @@ -307,7 +307,7 @@ namespace Acme.BookStore 示例注入了 `IRepository`(默认仓储). 它定义了标准的存储库方法并实现了 `IQueryable` 接口. -另外,身份模块定义了 `IIdentityUserRepository`(自定义仓储),你的应用程序也可以注入和使用它. `IIdentityUserRepository` 为 `IdentityUser` 实体提供了额外的定制方法,但它没有实现 `IQueryable`. +另外,身份模块定义了 `IIdentityUserRepository`(自定义仓储),你的应用程序也可以注入和使用它. `IIdentityUserRepository` 为 `IdentityUser` 实体提供了额外的定制方法,但它没有实现 `IQueryable`. ###### 创建一个新的实体 @@ -352,7 +352,7 @@ namespace Acme.BookStore.Roles * 它继承了[`AggregateRoot`类](Entities.md)和实现了[`IMultiTenant`]接口(Multi-Tenancy.md),因为 `IdentityRole` 也做了同样的继承. * 你可以添加 `IdentityRole` 实体定义的任何属性. 本例只加了 `TenantId` 和 `Name` 属性,因为我们这里只需要它们. 你可以把setters设置为私有(如同本例)以防意外更改身份模块的属性. * 你可以添加自定义(附加)属性. 本例添加了 `Title` 属性. -* **构造函数是私有的**,所以它不允许直接创建一个新的 `AppRole` 实体。创建角色身份模块的责任. 你可以查询角色,设置/更新自定义属性,但做为最佳实践你不应该在代码中创建和删除角色(尽管没有强制的限制). +* **构造函数是私有的**,所以它不允许直接创建一个新的 `AppRole` 实体.创建角色身份模块的责任. 你可以查询角色,设置/更新自定义属性,但做为最佳实践你不应该在代码中创建和删除角色(尽管没有强制的限制). 现在是时候定义EF Core映射. 打开应用程序的 `DbContext` (此示例中是 `BookStoreDbContext` )添加以下属性: @@ -360,7 +360,7 @@ namespace Acme.BookStore.Roles public DbSet Roles { get; set; } ```` -然后在 `OnModelCreating` 方法中配置映射(调用 `base.OnModelCreating(builder)` 之后): +然后在 `OnModelCreating` 方法中配置映射(调用 `base.OnModelCreating(builder)` 之后): ````csharp protected override void OnModelCreating(ModelBuilder builder) diff --git a/docs/zh-Hans/Entity-Framework-Core-MySQL.md b/docs/zh-Hans/Entity-Framework-Core-MySQL.md index 5f9d8023ab..15e2205c38 100644 --- a/docs/zh-Hans/Entity-Framework-Core-MySQL.md +++ b/docs/zh-Hans/Entity-Framework-Core-MySQL.md @@ -12,12 +12,12 @@ ## UseMySQL() -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseMySQL()`. 检查下列文件: +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseMySQL()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. +> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. ## 更改连接字符串 @@ -27,7 +27,7 @@ MySQL连接字符串与SQL Server连接字符串不同. 所以检查你的解决 ## 更改迁移DbContext -MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据您的DBMS配置字段. +MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据你的DBMS配置字段. 启动模板包含*YourProjectName*MigrationsDbContext,它负责维护和迁移数据库架构. 此DbContext基本上调用依赖模块的扩展方法来配置其数据库表. diff --git a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md index ca12d7524f..ca70d8b5c6 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md +++ b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md @@ -63,7 +63,7 @@ MySQL连接字符串与SQL Server连接字符串不同. 所以检查你的解决 ## 更改迁移DbContext -MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据您的DBMS配置字段. +MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据你的DBMS配置字段. 启动模板包含*YourProjectName*MigrationsDbContext,它负责维护和迁移数据库架构. 此DbContext基本上调用依赖模块的扩展方法来配置其数据库表. diff --git a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md index bb1de20207..08f8f405d1 100644 --- a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md +++ b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md @@ -12,12 +12,12 @@ ## UsePostgreSql() -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UsePostgreSql()`. 检查下列文件: +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UsePostgreSql()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. +> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. ## 更改连接字符串 diff --git a/docs/zh-Hans/Entity-Framework-Core-SQLite.md b/docs/zh-Hans/Entity-Framework-Core-SQLite.md index 4b8e009fe4..d75f8ba5b7 100644 --- a/docs/zh-Hans/Entity-Framework-Core-SQLite.md +++ b/docs/zh-Hans/Entity-Framework-Core-SQLite.md @@ -12,12 +12,12 @@ ## UseSqlite() -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseSqlite()`. 检查下列文件: +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseSqlite()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. +> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. ## 更改连接字符串 diff --git a/docs/zh-Hans/Entity-Framework-Core.md b/docs/zh-Hans/Entity-Framework-Core.md index d2af31f4d2..5ae6ce1070 100644 --- a/docs/zh-Hans/Entity-Framework-Core.md +++ b/docs/zh-Hans/Entity-Framework-Core.md @@ -108,7 +108,7 @@ protected override void OnModelCreating(ModelBuilder builder) ### 配置连接字符串选择 如果你的应用程序有多个数据库,你可以使用 `connectionStringName]` Attribute为你的DbContext配置连接字符串名称. -例: +例: ```csharp [ConnectionStringName("MySecondConnString")] @@ -274,7 +274,7 @@ public override async Task DeleteAsync( ## 访问 EF Core API -大多数情况下应该隐藏仓储后面的EF Core API(这也是仓储的设计目地). 但是如果想要通过仓储访问DbContext实现,则可以使用`GetDbContext()`或`GetDbSet()`扩展方法. 例: +大多数情况下应该隐藏仓储后面的EF Core API(这也是仓储的设计目地). 但是如果想要通过仓储访问DbContext实现,则可以使用`GetDbContext()`或`GetDbSet()`扩展方法. 例: ````csharp public class BookService @@ -304,7 +304,7 @@ public class BookService 默认,实体的所有额外属性存储在数据库的一个 `JSON` 对象中. -实体扩展系统允许你存储额外属性在数据库的单独字段中. 有关额外属性和实体扩展系统的更多信息,请参阅下列文档: +实体扩展系统允许你存储额外属性在数据库的单独字段中. 有关额外属性和实体扩展系统的更多信息,请参阅下列文档: * [自定义应用模块: 扩展实体](Customizing-Application-Modules-Extending-Entities.md) * [实体](Entities.md) @@ -313,7 +313,7 @@ public class BookService ### ObjectExtensionManager.Instance -`ObjectExtensionManager` 实现单例模式,因此你需要使用静态的 `ObjectExtensionManager.Instance` 来执行所有操作。 +`ObjectExtensionManager` 实现单例模式,因此你需要使用静态的 `ObjectExtensionManager.Instance` 来执行所有操作. ### MapEfCoreProperty @@ -417,7 +417,7 @@ context.Services.AddAbpDbContext(options => }); ```` -现在,您的自定义仓储也可以使用`IBookStoreDbContext`接口: +现在,你的自定义仓储也可以使用`IBookStoreDbContext`接口: ````csharp public class BookRepository : EfCoreRepository, IBookRepository diff --git a/docs/zh-Hans/Getting-Started-Angular-Template.md b/docs/zh-Hans/Getting-Started-Angular-Template.md index d2ec83293b..d242b4f39b 100644 --- a/docs/zh-Hans/Getting-Started-Angular-Template.md +++ b/docs/zh-Hans/Getting-Started-Angular-Template.md @@ -1,3 +1,8 @@ -## Getting Started With the Angular Application Template +# 启动模板入门 -TODO... \ No newline at end of file +请参阅以下教程,了解如何使用预构建的应用程序启动模板开始使用ABP框架: + +* [ASP.NET Core MVC / Razor Pages UI 入门](Getting-Started?UI=MVC&DB=EF&Tiered=No) +* [Angular UI 入门](Getting-Started?UI=NG&DB=EF&Tiered=No) + + \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md index 370e99c091..9d195562fd 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md @@ -2,7 +2,7 @@ 本教程将介绍如何开始以最少的依赖关系开始使用ABP开发. -通常情况下你需要下载一个 ***[启动模板](https://abp.io/Templates)*** +通常情况下你需要下载一个 ***[启动模板](Getting-Started-AspNetCore-MVC-Template.md)*** ### 创建一个新项目 @@ -156,7 +156,7 @@ services.AddApplication(options => }); ```` -4. 更新 `Program.cs`代码, 不再使用`WebHost.CreateDefaultBuilder()`方法(因为它使用默认的DI容器): +4. 更新 `Program.cs`代码, 不再使用`WebHost.CreateDefaultBuilder()`方法(因为它使用默认的DI容器): ````csharp public class Program diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md b/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md index 90291e968f..d242b4f39b 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md @@ -1,102 +1,8 @@ -## ASP.NET Core MVC 模板入门 +# 启动模板入门 -### 创建新项目 +请参阅以下教程,了解如何使用预构建的应用程序启动模板开始使用ABP框架: -本教程使用 **ABP CLI** 创建一个新项目. 更多选项, 请参阅[入门](https://abp.io/get-started)页面. +* [ASP.NET Core MVC / Razor Pages UI 入门](Getting-Started?UI=MVC&DB=EF&Tiered=No) +* [Angular UI 入门](Getting-Started?UI=NG&DB=EF&Tiered=No) -如果你之前未安装,请使用命令行安装ABP CLI: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -在空文件夹中使用 `abp new` 命令来创建项目: - -````bash -abp new Acme.BookStore -```` - -> 你可以使用不同级别的命名空间; 例如BookStore, Acme.BookStore或Acme.Retail.BookStore. - -`new` 命令创建**分层MVC应用程序**, **Entity Framework Core**作为数据库提供程序. 但是,它还有其他选择. 有关所有可用选项,请参见[CLI文档](CLI.md) - -#### 预先要求 - -创建项目的要求: - -* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) -* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) -* [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://classic.yarnpkg.com/) - -### 解决方案结构 - -在**Visual Studio**中打开解决方案: - -![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.png) - -该解决方案具有分层结构(基于[Domain Driven Design](Domain-Driven-Design.md)), 并包含配置好的的单元&集成测试项目,可与**EF Core**和**SQLite**数据库内存一起使用. - -> 请参阅[应用程序模板文档](Startup-Templates/Application.md)以详细了解解决方案结构. - -### 数据库连接字符串 - -查看`.Web`项目下`appsettings.json`文件中的 **连接字符串**: - -````json -{ - "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" - } -} -```` - -解决方案使用 **Entity Framework Core** 和 **MS SQL Server**. EF Core支持[各种](https://docs.microsoft.com/zh-cn/ef/core/providers/)数据库提供程序,因此你可以根据实际需要使用其他DBMS. 如果需要,请更改连接字符串. - -### 创建数据库并应用数据库迁移 - -你有两个选项来创建数据库. - -#### 使用DbMigrator应用程序 - -该解决方案包含一个控制台应用程序(在此示例中名为`Acme.BookStore.DbMigrator`),可以创建数据库,应用迁移和初始化数据. 它对开发和生产环境都很有用. - -> `.DbMigrator`项目有自己的`appsettings.json`. 因此,如果你更改了上面的连接字符串,则还应更改此字符串. - -右键单击`.DbMigrator`项目并选择 **设置为启动项目**: - -![set-as-startup-project](images/set-as-startup-project.png) - -按F5(或Ctrl + F5)运行应用程序. 它将具有如下所示的输出: - -![set-as-startup-project](images/db-migrator-app.png) - -#### 使用EF Core Update-Database命令 - -Ef Core具有`Update-Database`命令, 可根据需要创建数据库并应用挂起的迁移. 右键单击`.Web`项目并选择**设置为启动项目**: - -![set-as-startup-project](images/set-as-startup-project.png) - -打开**包管理器控制台(Package Manager Console)**, 选择`.EntityFrameworkCore.DbMigrations`项目作为**默认项目**并运行`Update-Database`命令: - -![pcm-update-database](images/pcm-update-database-v2.png) - -这将基于配置的连接字符串创建新数据库. - -> 使用`.Migrator`工具是建议的方法, 因为它还能初始化初始数据能够正确运行Web应用程序. - -### 运行应用程序 - -你现在可以运行应用程序,它将会打开**home**页面: - -![bookstore-homepage](images/bookstore-homepage.png) - -单击 **登录** 按钮, 输入用户名`admin`, 密码`1q2w3E*`, 登录应用程序. - -启动模板包括**身份管理**和**租户管理**模块. 登录后,将显示"管理"菜单, 你可以在其中管理**租户**,**角色**,**用户**和**权限**. 用户管理页面如下所示: - -![bookstore-user-management](images/bookstore-user-management-v2.png) - -### 下一步是什么? - -* [应用程序开发教程](Tutorials/AspNetCore-Mvc/Part-I.md) + \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started-With-Startup-Templates.md b/docs/zh-Hans/Getting-Started-With-Startup-Templates.md index 2b23350612..d242b4f39b 100644 --- a/docs/zh-Hans/Getting-Started-With-Startup-Templates.md +++ b/docs/zh-Hans/Getting-Started-With-Startup-Templates.md @@ -1,6 +1,8 @@ # 启动模板入门 -参阅下面的教程来学习如何开始使用的ABP框架预构建的应用程序启动模板: +请参阅以下教程,了解如何使用预构建的应用程序启动模板开始使用ABP框架: -* [ASP.NET Core MVC/Razor页面模板入门](Getting-Started-AspNetCore-MVC-Template.md) -* [Angular UI模板入门](Getting-Started-Angular-Template.md) \ No newline at end of file +* [ASP.NET Core MVC / Razor Pages UI 入门](Getting-Started?UI=MVC&DB=EF&Tiered=No) +* [Angular UI 入门](Getting-Started?UI=NG&DB=EF&Tiered=No) + + \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started.md b/docs/zh-Hans/Getting-Started.md new file mode 100644 index 0000000000..b60fccfaa2 --- /dev/null +++ b/docs/zh-Hans/Getting-Started.md @@ -0,0 +1,416 @@ +## 入门 + +````json +//[doc-params] +{ + "UI": ["MVC","NG"], + "DB": ["EF", "Mongo"], + "Tiered": ["Yes", "No"] +} +```` + +本教程介绍了如何创建一个新的{{if UI == "MVC"}} ASP.NET Core MVC web {{else if UI == "NG"}} Angular {{end}}. 配置并运行它. + +## 设置你的开发环境 + +创建第一个项目之前,需要正确的设置你的开发环境. + +### 预先要求 + +你需要安装以下工具: + +* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). +* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) + +* [Node v12+](https://nodejs.org) +* [Yarn v1.19+](https://classic.yarnpkg.com/) +{{ if Tiered == "Yes" }} + +* [Redis](https://redis.io/): 应用程序将Redis用作[分布式缓存](../Caching.md). 因此你需要安装并运行Redis. + +{{ end }} + +> 你可以也使用其他支持.NET Core 和 ASP.NET Core的编辑器. + +### 安装ABP CLI + +[ABP CLI](./CLI.md)是一个命令行页面,用于为基于ABP的应用程序验证和自动化一些任务. + +> ABP CLI是[ABP框架](https://abp.io/)一个免费开源的工具. + +你需要使用以下命令安排ABP CLI: + +````shell +dotnet tool install -g Volo.Abp.Cli +```` + +如果你已经安装,你可以使用以下命令更新到最新版本: + +````shell +dotnet tool update -g Volo.Abp.Cli +```` + +## 创建新项目 + +> 本文假设你使用 **{{ UI_Value }}** 做为UI框架 **{{ DB_Value }}** 做为数据库提供程序,对于其它选项,你可以更改文档顶部的首选项. + +### 使用ABP CLI创建一个新项目 + +使用ABP CLI的 `new` 命令创建新项目: + +````shell +abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes" && UI != "NG"}} --tiered {{else if Tiered == "Yes" && UI == "NG"}}--separate-identity-server{{end}} +```` + +* `-t` 参数指定 [启动模板](Startup-Templates/Application.md) 名称. `app` 是一个启动模板名称,包含了预安装并且配置好的[ABP模块](Modules/Index.md). + +{{ if UI == "NG" }} + +* `-u` 指定UI框架, 本例中是 `angular`. + +{{ if Tiered == "Yes" }} + +* `--separate-identity-server` 参数用于将Identity服务器应用程序与API主机应用程序分隔开. 如果未指定,你将只有一个端点. + +{{ end }} + +{{ end }} + +{{ if DB == "Mongo" }} + +* `-d` 指定数据库提供程序, 本例中是 `mongodb`. + +{{ end }} + +{{ if Tiered == "Yes" && UI != "NG" }} + +* `--tiered` 参数用于创建n层解决方案,其中身份验证服务器层,UI层和API层在物理上是分离的. + +{{ end }} + +> 你可以使用不同级别的命令空间; 例如. BookStore, Acme.BookStore or Acme.Retail.BookStore. + +## 解决方案结构 + +{{ if UI == "MVC" }} + +创建项目后你会有以下解决方案目录和文件: + +![](images/solution-files-mvc.png) + +在Visual Studio中打开 `.sln` 文件时,将看到以下解决方案结构: + +{{if DB == "Mongo"}} + +![vs-default-app-solution-structure](images/vs-app-solution-structure-mongodb.png) + +{{else}} + +![vs-default-app-solution-structure](images/vs-app-solution-structure{{if Tiered == "Yes"}}-tiered{{end}}.png) + +{{end}} + +{{ else if UI == "NG" }} +在创建的解决方案中有三个文件夹: + +![](images/solution-files-non-mvc.png) + +* `angular` 文件夹包含Angular UI应用程序. +* `aspnet-core` 文件夹包含后端应用程序. +* `react-native` 文件夹包含React Native UI 应用程序. + +打开 `aspnet-core` 文件夹下的 `.sln`(`Visual Studio`解决方案)文件: +![vs-angular-app-backend-solution-structure](images/vs-spa-app-backend-structure{{if DB == "Mongo"}}-mongodb{{end}}.png) + +{{ end }} + +> ###### 关于解决方案中的项目 +> +> 根据你的**UI**,**数据库**和其他选项,你的解决方案的结构可能略有不同. + +该解决方案具有分层结构(基于[Domain Driven Design](Domain-Driven-Design.md)), 并包含配置好的的单元&集成测试项目. + +{{ if DB == "EF" }} + +集成测试项目已配置为可与 **EF Core** & **SQLite 内存** database同时使用. + +{{ else if DB == "Mongo" }} + +集成测试项目已配置为每个测试创建的内存中的**MongoDB**数据库(使用的[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)库). + +{{ end }} + +> 请参阅[应用程序模板文档](Startup-Templates/Application.md)详细了解解决方案结构. + +## 创建数据库 + +### 数据库连接字符串 + +检查 {{if UI == "MVC"}}{{if Tiered == "Yes"}}`.IdentityServer` 和 `.HttpApi.Host` 项目{{else}}`.Web` 项目{{end}}{{else if UI == "NG" }}`.HttpApi.Host` 项目{{end}}下 `appsettings.json` 文件中的 **链接字符串**: + +{{ if DB == "EF" }} + +````json +"ConnectionStrings": { + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" +} +```` + +该解决方案配置为**Entity Framework Core**与**MS SQL Server**一起使用. EF Core支持[各种](https://docs.microsoft.com/en-us/ef/core/providers/)数据库提供程序,因此你可以使用任何受支持的DBMS. 请参阅[Entity Framework集成文档](https://docs.abp.io/en/abp/latest/Entity-Framework-Core)了解如何切换到另一个DBMS. + +### 数据库连接字符串 + +查看`.Web`项目下`appsettings.json`文件中的 **连接字符串**: + +````json +{ + "ConnectionStrings": { + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" + } +} +```` + +解决方案使用 **Entity Framework Core** 和 **MS SQL Server**. EF Core支持[各种](https://docs.microsoft.com/zh-cn/ef/core/providers/)数据库提供程序,因此你可以根据实际需要使用其他DBMS. 如果需要,请更改连接字符串. + +### 应用迁移 + +该解决方案使用[Entity Framework Core Code First 迁移](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). 你需要应用迁移来创建数据库,有两种方法迁移数据库. + +#### 使用DbMigrator应用程序应用迁移 + +该解决方案包含一个控制台应用程序(在此示例中名为`Acme.BookStore.DbMigrator`),可以创建数据库,应用迁移和初始化数据. 它对开发和生产环境都很有用. + +> `.DbMigrator`项目有自己的`appsettings.json`. 因此,如果你更改了上面的连接字符串,则还应更改此字符串. + +右键单击`.DbMigrator`项目并选择 **设置为启动项目**: + +![set-as-startup-project](images/set-as-startup-project.png) + +按F5(或Ctrl + F5)运行应用程序. 它将具有如下所示的输出: + +![set-as-startup-project](images/db-migrator-app.png) + +#### 使用EF Core Update-Database命令 + +Ef Core具有`Update-Database`命令, 可根据需要创建数据库并应用挂起的迁移. 右键单击`.Web`项目并选择**设置为启动项目**: + +{{ if UI == "MVC" }} + +右键单击{{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}}项目并选择**设置为启动项目**: + +{{ else if UI != "MVC" }} + +右键单击`.HttpApi.Host`项目并选择**设置为启动项目**: + +{{ end }} + +![set-as-startup-project](images/set-as-startup-project.png) + +打开**包管理器控制台(Package Manager Console)**, 选择`.EntityFrameworkCore.DbMigrations`项目作为**默认项目**并运行`Update-Database`命令: + +![package-manager-console-update-database](images/package-manager-console-update-database.png) + +这将基于配置的连接字符串创建新数据库. + +> 使用`.Migrator`工具是建议的方法, 因为它还能初始化初始数据能够正确运行Web应用程序. + +{{ else if DB == "Mongo" }} + +````json +"ConnectionStrings": { + "Default": "mongodb://localhost:27017/BookStore" +} +```` + +该解决方案被配置为在你的本地计算机中使用 **MongoDB**,因此你需要启动并运行一个MongoDB服务器实例或者将连接字符串更改为另一个MongoDB服务器. + +### 初始化种子数据 + +该解决方案附带一个 `.DbMigrator` 控制台应用程序,该应用程序为初始数据提供了种子. 它对于开发以及生产环境都很有用. + +> `.DbMigrator` 项目有自己的 `appsettings.json`.如果你更改了其他项目的 `appsettings.json`,也应该更改这个. + +右键点击 `.DbMigrator` 并选择 **设置为启动项目**. + +![set-as-startup-project](images/set-as-startup-project.png) + + 按F5(或Ctrl+F5)启动应用程序,你会看到以下输出: + + ![db-migrator-output](images/db-migrator-output.png) + +> 数据库创建后会初始化种子数据, 其中包含用于登录的 `admin` 用户. 所以你至少使用 `.DbMigrator` 一次. + +{{ end }} + +### 运行应用程序 + +{{ if UI == "MVC" }} + +{{ if Tiered == "Yes" }} + +确保 `.IdentityServer` 是启动项目,运行应用程序后会在你的浏览器打开一个 **login** 页面. + +> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间. + +你可以登录,但是不能在这里进入主应用程序,它仅是验证服务器. + +确保 `.HttpApi.Host` 是启动项目,运行应用程序后会在你的浏览器打开一个 **Swagger UI** 页面. + +![swagger-ui](images/swagger-ui.png) + +这里是Web应用程序使用的API应用程序. + +最后确保 `.Web` 是启动项目,运行应用程序后会在你的浏览器打开一个 **welcome** 页面. + +![mvc-tiered-app-home](images/bookstore-home.png) + +点击 **login** 按钮重定向到 `Identity Server` 来登录应用程序. + +![bookstore-login](images/bookstore-login.png) + +{{ else }} + +最后确保 `.Web` 是启动项目,运行应用程序后会在你的浏览器打开一个 **login** 页面. + +> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间. + +![bookstore-login](images/bookstore-login.png) + +{{ end }} + +{{ else if UI != "MVC" }} + +#### 运行HTTP API Host (服务器端) + +{{ if Tiered == "Yes" }} + +确保 `.IdentityServer` 是启动项目,运行应用程序后会在你的浏览器打开一个 **login** 页面. + +> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间. + +你可以登录,但是不能在这里进入主应用程序,它仅是验证服务器. + +{{ end }} + +确保 `.HttpApi.Host` 是启动项目,运行应用程序后会在你的浏览器打开一个 **Swagger UI** 页面. + +{{ if Tiered == "No" }} + +> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间. + +{{ end }} + +![swagger-ui](images/swagger-ui.png) + +你可以看到应用程序的API并进行测试. 更多信息,请参阅[Swagger UI](https://swagger.io/tools/swagger-ui/). + +> ##### Swagger UI 授权 +> +> 大多数的HTTP API都需要身份验证和授权. 如果你要测试授权API, 请手动进入 `/Account/Login` 页面, 输入用户名: `admin` 和密码: `1q2w3E*` 登录到应用程序. 然后你可以访问授权API. + +{{ end }} + +{{ if UI == "NG" }} +#### 运行 Angular 应用程序 (客户端) + +在 `angular` 下打开命令行终端, 输入 `yarn` 命令(我们推荐使用[yarn](https://yarnpkg.com/)包管理, `npm install` 在大多数情况下也可以工作). + +```bash +yarn +``` + +等到所有node模块加载成功, 执行 `yarn start` (或 `npm start`) 命令: + +```bash +yarn start +``` + +等待 `Angular CLI` 使用 `BrowserSync` 启动 `Webpack` dev-server. +它会负责编译你的 `TypeScript`代码, 并自动重新加载浏览器. +完成后 `Angular Live Development Server` 会监听 localhost:4200. +打开你的浏览器并导航到[localhost:4200](http://localhost:4200/). + +![bookstore-login](images/bookstore-login.png) + +{{ end }} + +输入用户名 **admin**,密码 **1q2w3E*** 登录到应用程序. + +![bookstore-home](images/bookstore-home.png) + +应用程序已经启动并执行,你可以基于该启动模板开发应用程序. + +#### 移动开发 + +ABP平台提供了[React Native](https://reactnative.dev/)模板用于开发移动应用程序. + +> 该解决方案默认 `react-native` 包含了React Native应用程序,如果你不计划使用React Native开发移动应用程序,你可以忽略并删除 `react-native` 文件夹. + +运行在Android模拟器或真机上的React Native应用程序无法连接到 `localhost` 上的后.要修复此问题,需要在本地IP上运行后端. + +{{ if Tiered == "No"}} +![React Native host project local IP entry](images/rn-host-local-ip.png) + +* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 和 `Authority` 属性的 `localhost` 替换为你本地的IP地址. +* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. + +{{ else if Tiered == "Yes" }} + +![React Native tiered project local IP entry](images/rn-tiered-local-ip.png) + +* 打开 `.IdentityServer` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 属性的 `localhost` 替换为你本地的IP地址. +* 打开 `.IdentityServer/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. +* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `Authority` 属性的 `localhost` 替换为你本地的IP地址. +* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. + +{{ end }} + +按照**运行HTTP API Host (服务端口)**那样运行后端. + +> React Native应用程序不信任自动生成的.NET HTTPS证书,你可以在开发期间使用HTTP. + +在 `react-native` 文件夹打开命令行终端,输入 `yarn` 命令(我们推荐使用[yarn](https://yarnpkg.com/)包管理, `npm install` 在大多数情况下也可以工作). + +```bash +yarn +``` + +* 打开 `react-nativer` 文件夹下的 `Environment.js` 文件. 将 `apiUrl` 和 `issuer` 属性的 `localhost` 替换为你本地的IP地址: + +![react native environment local IP](images/rn-environment-local-ip.png) + +{{ if Tiered == "Yes" }} + +> 确保 `issuer` 与正在运行的 `.IdentityServer` 项目匹配, `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配. + +{{else}} + +> 确保 `issuer` 和 `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配 + +{{ end }} + +等到所有node模块加载成功, 执行 `yarn start` (或 `npm start`) 命令: + +```bash +yarn start +``` + +等待Expo CLI启动后Expo CLI在 `http://localhost:19002/` 地址要开管理页面. + +![expo-interface](images/rn-expo-interface.png) + +在上面的管理界面中,可以通过使用[Expo Client](https://expo.io/tools#client)扫描二维码,使用Android模拟器,iOS模拟器或真机来启动应用程序. + +> 请参阅[expo.io](https://docs.expo.io/versions/v36.0.0/workflow/ios-simulator/)上的[Android Studio模拟器](https://docs.expo.io/versions/v36.0.0/workflow/android-studio-emulator/)和[iOS模拟器文档](https://docs.expo.io/versions/v36.0.0/workflow/android-studio-emulator/). + +![React Native login screen on iPhone 11](images/rn-login-iphone.png) + +输入用户名 **admin**,密码 **1q2w3E*** 登录到应用程序. + +应用程序已经启动并执行,你可以基于该启动模板开发应用程序. + +> [应用程序启动模板](Startup-Templates/Application.md) 包含租户管理和Identity模块. + +## 下一步是什么? + +[应用程序开发教程](Tutorials/Part-1.md) diff --git a/docs/zh-Hans/Index.md b/docs/zh-Hans/Index.md index d946d8f0bb..71e5a520da 100644 --- a/docs/zh-Hans/Index.md +++ b/docs/zh-Hans/Index.md @@ -12,7 +12,7 @@ ABP是一个**开源应用程序框架**,专注于基于ASP.NET Core的Web应用 * [ASP.NET Core MVC 模板](Getting-Started-AspNetCore-MVC-Template.md) -如果您想从头开始(使用空项目),请手动安装ABP框架并使用以下教程: +如果你想从头开始(使用空项目),请手动安装ABP框架并使用以下教程: * [控制台应用程序](Getting-Started-Console-Application.md) * [ASP.NET Core Web 应用程序](Getting-Started-AspNetCore-Application.md) diff --git a/docs/zh-Hans/Localization.md b/docs/zh-Hans/Localization.md index fce4b14407..1292655e62 100644 --- a/docs/zh-Hans/Localization.md +++ b/docs/zh-Hans/Localization.md @@ -86,6 +86,21 @@ JSON文件位于 "/Localization/Resources/Test" 项目文件夹下, 如下图所 * 每个本地化文件都需要定义 `culture` (文化) 代码 (例如 "en" 或 "en-US"). * `texts` 部分只包含本地化字符串的键值集合 (键也可能有空格). +### 默认资源 + +可以将 `AbpLocalizationOptions.DefaultResourceType` 设置为资源类型,在未指定本地化资源时使用: + +````csharp +Configure(options => +{ + options.DefaultResourceType = typeof(TestResource); +}); +```` + +> [启动模板]](Startup-Templates/Application.md) 设置 `DefaultResourceType` 为应用程序的本地化资源. + +请参阅下面的*客户端*部分获取用例 + ##### 简短的本地化资源名称 本地化资源也可以在客户端(JavaScript)使用. 因此, 为本地化资源设置一个简短的名称可以更方便的本地化文本. 例如: @@ -165,6 +180,10 @@ public class MyService } ```` +##### 格式参数 + +格式参数可以在本地化Key参数后传递,如果你的消息是 `Hello {0}, welcome!`,可以将 `{0}` 传递给localizer,例如: `_localizer["HelloMessage", "John"]`. + ###### 在Razor视图/Page中简单的用法 ````c# @@ -179,7 +198,9 @@ public class MyService ABP提供了JavaScript服务, 可以在客户端使用相同的本地化文本. -获取本地化资源: +#### getResource + +`abp.localization.getResource` 函数用于获取本地化资源: ````js var testResource = abp.localization.getResource('Test'); @@ -191,6 +212,33 @@ var testResource = abp.localization.getResource('Test'); var str = testResource('HelloWorld'); ```` -## See Also +#### 本地化 + +`abp.localization.localize` 函数用于获取本地化文本,你可以传递本地化Key和资源名称: + +````js +var str = abp.localization.localize('HelloWorld', 'Test'); +```` + +`HelloWorld` 是本地化文本的Key, `Test` 是本地化资源的名称. + +如果未指定本地化资源名称,它使用 `AbpLocalizationOptions` 中定义的默认本地化资源(参见上面的*默认资源*部分). 例: + +````js +var str = abp.localization.localize('HelloWorld'); //uses the default resource +```` + +##### 格式参数 + +如果本地化字符串包含参数, 例如 `Hello {0}, welcome!`. 你可以将参数传递给本地化方法. 例: + +````js +var str1 = abp.localization.getResource('Test')('HelloWelcomeMessage', 'John'); +var str2 = abp.localization.localize('HelloWorld', 'Test', 'John'); +```` + +上面的两个示例都会输出 `Hello John, welcome!`. + +## 另请参阅 * [Angular UI中的本地化](UI/Angular/Localization.md) \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Docs.md b/docs/zh-Hans/Modules/Docs.md index 20382d24f2..96452f2bd5 100644 --- a/docs/zh-Hans/Modules/Docs.md +++ b/docs/zh-Hans/Modules/Docs.md @@ -326,7 +326,7 @@ There are no projects yet! {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs/zh-Hans/","GitHubAccessToken":"***","GitHubUserAgent":""} ``` - 注意 `GitHubAccessToken` 用 `***` 掩盖. 这是一个私人令牌,你必须从GitHub获取它. 请参阅 https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ + 注意 `GitHubAccessToken` 用 `***` 掩盖. 这是一个私人令牌,你必须从GitHub获取它. 请参阅 https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ - MainWebsiteUrl: `/` @@ -338,7 +338,7 @@ There are no projects yet! INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""}', N'/', N'master', N'') ``` -请注意,`GitHubAccessToken` 被屏蔽了.它是一个私人令牌,你必须获得自己的令牌并替换 `***` 字符串. +请注意,`GitHubAccessToken` 被屏蔽了.它是一个私人令牌,你必须获得自己的令牌并替换 `***` 字符串. 现在你可以运行应用程序并导航到 `/Documents`. @@ -378,9 +378,9 @@ INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocume INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs"}', N'/', NULL, N'') ``` -添加上面的一个示例项目后运行该应用程序. 在菜单中你会看到`文档` 链接,点击菜单链接打开文档页面. +添加上面的一个示例项目后运行该应用程序. 在菜单中你会看到`文档` 链接,点击菜单链接打开文档页面. -到目前为止, 我们已经从abp.io网站创建了一个新的应用程序,并为Docs模块做好准备. +到目前为止, 我们已经从abp.io网站创建了一个新的应用程序,并为Docs模块做好准备. ### 7- 添加一个新文档 @@ -454,7 +454,7 @@ public class Person } ``` -因为并不是项目中的每个文档都有章节或者不需要所有的参数,你必须声明哪些参数将用于对文档进行分段,在文档的任何地方都可以使用JSON块. +因为并不是项目中的每个文档都有章节或者不需要所有的参数,你必须声明哪些参数将用于对文档进行分段,在文档的任何地方都可以使用JSON块. 例如 [Getting-Started.md](https://github.com/abpio/abp-commercial-docs/blob/master/en/getting-started.md): @@ -570,7 +570,7 @@ This document assumes that you prefer to use **{{ UI_Value }}** as the UI framew ![Navigation menu](../images/docs-module_download-sample-navigation-menu.png) -最后,为您的项目添加了一个新的Docs模块, 该模块由GitHub提供. +最后,为你的项目添加了一个新的Docs模块, 该模块由GitHub提供. ## 全文搜索(Elastic Search) diff --git a/docs/zh-Hans/Object-Extensions.md b/docs/zh-Hans/Object-Extensions.md index 27957708f4..f925a8730a 100644 --- a/docs/zh-Hans/Object-Extensions.md +++ b/docs/zh-Hans/Object-Extensions.md @@ -62,7 +62,7 @@ if (user.GetProperty("IsSuperUser")) ##### 非基本属性类型 -如果您的属性类型不是原始类型(int,bool,枚举,字符串等),你需要使用 `GetProperty` 的非泛型版本,它会返回 `object`. +如果你的属性类型不是原始类型(int,bool,枚举,字符串等),你需要使用 `GetProperty` 的非泛型版本,它会返回 `object`. #### HasProperty @@ -176,11 +176,111 @@ ObjectExtensionManager.Instance `options` 有一个名为 `Configuration` 的字典,该字典存储对象扩展定义甚至可以扩展. EF Core使用它来将其他属性映射到数据库中的表字段. 请参阅[扩展实体文档](Customizing-Application-Modules-Extending-Entities.md). +#### CheckPairDefinitionOnMapping + +控制在映射两个可扩展对象时如何检查属性定义. 请参阅*对象到对象映射*部分,了解 `CheckPairDefinitionOnMapping` 选项. + +## Validation + +你可能要为你定义的额外属性添加一些 **验证规则**. `AddOrUpdateProperty` 方法选项允许进行验证的方法有两种: + +1. 你可以为属性添加 **数据注解 attributes**. +2. 你可以给定一个action(代码块)执行 **自定义验证**. + +当你在**自动验证**的方法(例如:控制器操作,页面处理程序方法,应用程序服务方法...)中使用对象时,验证会工作. 因此,每当扩展对象被验证时,所有额外的属性都会被验证. + +### 数据注解 Attributes + +所有标准的数据注解Attributes对于额外属性都是有效的. 例: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.Attributes.Add(new RequiredAttribute()); + options.Attributes.Add( + new StringLengthAttribute(32) { + MinimumLength = 6 + } + ); + }); +```` + +使用以上配置,如果没有提供有效的 `SocialSecurityNumber` 值, `IdentityUserCreateDto` 对象将是无效的. + +### 自定义验证 + +如果需要,可以添加一个自定义action验证额外属性. 例: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.Validators.Add(context => + { + var socialSecurityNumber = context.Value as string; + + if (socialSecurityNumber == null || + socialSecurityNumber.StartsWith("X")) + { + context.ValidationErrors.Add( + new ValidationResult( + "Invalid social security number: " + socialSecurityNumber, + new[] { "SocialSecurityNumber" } + ) + ); + } + }); + }); +```` + +`context.ServiceProvider` 可以解析服务. + +除了为单个属性添加自定义验证逻辑外,还可以添加在对象级执行的自定义验证逻辑. 例: + +````csharp +ObjectExtensionManager.Instance +.AddOrUpdate(objConfig => +{ + //Define two properties with their own validation rules + + objConfig.AddOrUpdateProperty("Password", propertyConfig => + { + propertyConfig.Attributes.Add(new RequiredAttribute()); + }); + + objConfig.AddOrUpdateProperty("PasswordRepeat", propertyConfig => + { + propertyConfig.Attributes.Add(new RequiredAttribute()); + }); + + //Write a common validation logic works on multiple properties + + objConfig.Validators.Add(context => + { + if (context.ValidatingObject.GetProperty("Password") != + context.ValidatingObject.GetProperty("PasswordRepeat")) + { + context.ValidationErrors.Add( + new ValidationResult( + "Please repeat the same password!", + new[] { "Password", "PasswordRepeat" } + ) + ); + } + }); +}); +```` + ## 对象到对象映射 假设你已向可扩展的实体对象添加了额外的属性并使用了自动[对象到对象的映射](Object-To-Object-Mapping.md)将该实体映射到可扩展的DTO类. 在这种情况下你需要格外小心,因为额外属性可能包含**敏感数据**,这些数据对于客户端不可用. -本节提供了一些**好的做法**,可以控制对象映射的额外属性。 +本节提供了一些**好的做法**,可以控制对象映射的额外属性. ### MapExtraPropertiesTo @@ -232,7 +332,7 @@ identityUser.MapExtraPropertiesTo( #### AutoMapper集成 -如果您使用的是[AutoMapper](https://automapper.org/)库,ABP框架还提供了一种扩展方法来利用上面定义的 `MapExtraPropertiesTo` 方法. +如果你使用的是[AutoMapper](https://automapper.org/)库,ABP框架还提供了一种扩展方法来利用上面定义的 `MapExtraPropertiesTo` 方法. 你可以在映射配置文件中使用 `MapExtraProperties()` 方法. @@ -247,7 +347,7 @@ public class MyProfile : Profile } ```` -它与 `MapExtraPropertiesTo()` 方法具有相同的参数。 +它与 `MapExtraPropertiesTo()` 方法具有相同的参数. ## Entity Framework Core 数据库映射 diff --git a/docs/zh-Hans/Object-To-Object-Mapping.md b/docs/zh-Hans/Object-To-Object-Mapping.md index 57b744e0bf..1b60112cb2 100644 --- a/docs/zh-Hans/Object-To-Object-Mapping.md +++ b/docs/zh-Hans/Object-To-Object-Mapping.md @@ -168,7 +168,7 @@ public class MyProfile : Profile 假设你已经创建了一个**可重用的模块**,其中定义了AutoMapper配置文件,并在需要映射对象时使用 `IObjectMapper`. 根据[模块化](Module-Development-Basics.md)的性质,你的模块可以用于不同的应用程序. -`IObjectMapper` 是一个抽象,可以由最终应用程序替换使用另一个映射库. 这里的问题是你的可重用模块设计为使用AutoMapper,因为它为其定义映射配置文件. 这种情况下即使最终应用程序使用另一个默认对象映射库,你也要保证模块始终使用AutoMapper. +`IObjectMapper` 是一个抽象,可以由最终应用程序替换使用另一个映射库. 这里的问题是你的可重用模块设计为使用AutoMapper,因为它为其定义映射配置文件. 这种情况下即使最终应用程序使用另一个默认对象映射库,你也要保证模块始终使用AutoMapper. `IObjectMapper`将对象映射器上下文化,你可以为不同的 模块/上下文 使用不同的库. @@ -201,7 +201,7 @@ public class UserAppService : ApplicationService `UserAppService` 注入 `IObjectMapper`, 它是模块的特定对象映射器,用法与 `IObjectMapper` 完全相同. -上面的示例代码未使用 `ApplicationService` 中定义的 `ObjectMapper` 属性,而是注入了 `IObjectMapper`. 但是 `ApplicationService` 定义了可以在类构造函数中设置的 `ObjectMapperContext` 属性, 因此仍然可以使用基类属性. 示例可以进行以下重写: +上面的示例代码未使用 `ApplicationService` 中定义的 `ObjectMapper` 属性,而是注入了 `IObjectMapper`. 但是 `ApplicationService` 定义了可以在类构造函数中设置的 `ObjectMapperContext` 属性, 因此仍然可以使用基类属性. 示例可以进行以下重写: ````csharp public class UserAppService : ApplicationService diff --git a/docs/zh-Hans/Options.md b/docs/zh-Hans/Options.md index 18ccd90bed..c88ff0adda 100644 --- a/docs/zh-Hans/Options.md +++ b/docs/zh-Hans/Options.md @@ -9,7 +9,7 @@ ABP框架遵循选项模式,并定义了用于配置框架和模块的选项类( ## 配置选项 通常配置选项在 `Startup` 类的 `ConfigureServices` 方法中. 但由于ABP框架提供了模块化基础设施,因此你可以在[模块](Module-Development-Basics.md)的`ConfigureServices` 方法配置选项. -例: +例: ````csharp public override void ConfigureServices(ServiceConfigurationContext context) @@ -34,7 +34,7 @@ public class MyOptions } ```` -然后开发人员可以像上面 `AbpAuditingOptions` 示例一样配置你的选项: +然后开发人员可以像上面 `AbpAuditingOptions` 示例一样配置你的选项: ````csharp public override void ConfigureServices(ServiceConfigurationContext context) @@ -80,9 +80,9 @@ public class MyService : ITransientDependency 如果你正在开发一个模块,可能需要让开发者能够设置一些选项,并在依赖注入注册阶段使用这些选项. 你可能需要根据选项值配置其他服务或更改依赖注入的注册代码. -对于此类情况,ABP为 `IServiceCollection` 引入了 `PreConfigure` 和 `ExecutePreConfiguredActions` 扩展方法. 该模式的工作原理如下所述。 +对于此类情况,ABP为 `IServiceCollection` 引入了 `PreConfigure` 和 `ExecutePreConfiguredActions` 扩展方法. 该模式的工作原理如下所述. -1. 你的模块中定义计划选项类. 例: +1. 你的模块中定义计划选项类. 例: ````csharp public class MyPreOptions @@ -92,7 +92,7 @@ public class MyPreOptions ```` 然后任何依赖于模块的模块类都可以在其 `PreConfigureServices` 方法中使用 `PreConfigure` 方法. -例: +例: ````csharp public override void PreConfigureServices(ServiceConfigurationContext context) diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md index 9b559797f7..40cb42cda6 100644 --- a/docs/zh-Hans/Samples/Microservice-Demo.md +++ b/docs/zh-Hans/Samples/Microservice-Demo.md @@ -51,13 +51,13 @@ ABP框架的主要目标之一就是提供[便捷的基础设施来创建微服 ### 创建数据库 -MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库的结构。其实你可以很轻松的创建数据库,因为这个解决方案配置了使用 Entity Core Code First 来做迁移。 +MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库的结构.其实你可以很轻松的创建数据库,因为这个解决方案配置了使用 Entity Core Code First 来做迁移. -这个解决方案中有两个 SQL server 数据库。 +这个解决方案中有两个 SQL server 数据库. #### MsDemo_Identity 数据库 -* 右键 `AuthServer.Host` 项目,然后点击 `设置为启动项目`. +* 右键 `AuthServer.Host` 项目,然后点击 `设置为启动项目`. * 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) * 选择 `AuthServer.Host` 成为 **默认项目**. * 执行 `Update-Database` 命令. @@ -66,7 +66,7 @@ MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库 #### MsDemo_ProductManagement -* 右键 `ProductService.Host` 项目,然后点击 `设置为启动项目`. +* 右键 `ProductService.Host` 项目,然后点击 `设置为启动项目`. * 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) * 选择 `ProductService.Host` 成为 **默认项目**. * 执行 `Update-Database` 命令. @@ -334,7 +334,7 @@ context.Services.AddAuthentication(options => - 它需要额外的身份范围 *role*, *email* and *phone*. - 它需要API资源范围 *PublicWebSiteGateway*,*BloggingService*和*ProductService*,因为它将这些服务用作API. -IdentityServer客户端设置存储在`appsettings.json`文件中: +IdentityServer客户端设置存储在`appsettings.json`文件中: ```json "AuthServer": { @@ -348,7 +348,7 @@ IdentityServer客户端设置存储在`appsettings.json`文件中: PublicWebSite.Host项目有一个列出产品的页面 (`Pages/Products.cshtml`). 它还使用博客模块中的UI. 为此`PublicWebSiteHostModule`加入了`BloggingWebModule`(*[Volo.Blogging.Web](https://www.nuget.org/packages/Volo.Blogging.Web)* 包)的依赖项. -产品页面的屏幕截图: +产品页面的屏幕截图: ![microservice-sample-public-product-list](../images/microservice-sample-public-product-list.png) @@ -390,7 +390,7 @@ PublicWebSite.Host项目有一个列出产品的页面 (`Pages/Products.cshtml`) #### 远程服务配置 -`appsettings.json`文件中的`RemoteService`配置很简单: +`appsettings.json`文件中的`RemoteService`配置很简单: ````json "RemoteServices": { @@ -418,7 +418,7 @@ PublicWebSite.Host项目有一个列出产品的页面 (`Pages/Products.cshtml`) } ```` -此示例使用`client_credentials` 授予类型,该类型需要`ClientId`和`ClientSecret`进行身份验证过程. 还有[其他授予类型](http://docs.identityserver.io/en/latest/topics/grant_types.html). 例如, 你可以使用以下配置切换到`password`(Resource Owner Password)授予类型: +此示例使用`client_credentials` 授予类型,该类型需要`ClientId`和`ClientSecret`进行身份验证过程. 还有[其他授予类型](http://docs.identityserver.io/en/latest/topics/grant_types.html). 例如, 你可以使用以下配置切换到`password`(Resource Owner Password)授予类型: ````json "IdentityClients": { @@ -571,7 +571,7 @@ app.UseOcelot().Wait(); #### 权限管理 -后端管理应用程序提供权限管理UI(之前见过),并使用此网关获取/设置权限. 权限管理API托管在网关内,而不是单独的服务. 这是一个设计决策,但如果您愿意,它可以作为另一个微服务托管. +后端管理应用程序提供权限管理UI(之前见过),并使用此网关获取/设置权限. 权限管理API托管在网关内,而不是单独的服务. 这是一个设计决策,但如果你愿意,它可以作为另一个微服务托管. #### Dependencies @@ -1229,7 +1229,7 @@ public class ProductCodeAlreadyExistsException : BusinessException } ```` -`PM:000001`是发送给客户端的异常类型的代码,因此他们可以理解错误类型. 在这种情况下没有实现,但也可以本地化业务异常. 请参阅[异常处理文档](../Exception-Handling.md). +`PM:000001`是发送给客户端的异常类型的代码,因此他们可以理解错误类型. 在这种情况下没有实现,但也可以本地化业务异常. 请参阅[异常处理文档](../Exception-Handling.md). #### 应用层 @@ -1278,7 +1278,7 @@ public async Task UpdateAsync(Guid id, UpdateProductDto input) 分布式事件(事件总线)是一种消息传递方式,其中服务引发/触发事件,而其他服务注册/侦听这些事件,以便在发生重要事件时得到通知. ABP通过提供约定,服务和集成使分布式事件更易于使用. -您已经看到`Product`类使用以下代码行发布事件: +你已经看到`Product`类使用以下代码行发布事件: ````csharp AddDistributedEvent(new ProductStockCountChangedEto(Id, StockCount, stockCount)); @@ -1406,7 +1406,7 @@ Kibana URL默认为`http://localhost:5601/`. ABP提供自动审计日志记录,详细保存每个请求(当前用户,浏览器/客户端,执行了哪些操作,哪些实体更改,甚至实体的哪些属性已更新). 有关详细信息,请参阅[审计日志文档](../Audit-Logging.md). -所有服务和应用程序都配置为编写审核日志. 审核日志将保存到MsDemo_Identity SQL数据库中. 因此,您可以从单个点查询所有应用程序的所有审核日志. +所有服务和应用程序都配置为编写审核日志. 审核日志将保存到MsDemo_Identity SQL数据库中. 因此,你可以从单个点查询所有应用程序的所有审核日志. 审核日志记录具有`CorrelationId`属性,可用于跟踪请求. 当服务在单个Web请求中调用另一个服务时,它们都会使用相同的`CorrelationId`保存审核日志. 请参阅数据库中的`AbpAuditLogs`表. diff --git a/docs/zh-Hans/Settings.md b/docs/zh-Hans/Settings.md index 25559082f2..a4d84c91b7 100644 --- a/docs/zh-Hans/Settings.md +++ b/docs/zh-Hans/Settings.md @@ -226,6 +226,6 @@ Configure(options => ## 设置管理模块 -设置系统核心是相当独立的,不做任何关于如何管理(更改)设置值的假设. 默认的`ISettingStore`实现也是`NullSettingStore`,它为所有设置值返回null. +设置系统核心是相当独立的,不做任何关于如何管理(更改)设置值的假设. 默认的`ISettingStore`实现也是`NullSettingStore`,它为所有设置值返回null. 设置管理模块通过管理数据库中的设置值来完成逻辑(实现`ISettingStore`).有关更多信息参阅[设置管理模块](Modules/Setting-Management.md)学习更多. diff --git a/docs/zh-Hans/Specifications.md b/docs/zh-Hans/Specifications.md new file mode 100644 index 0000000000..f927c80255 --- /dev/null +++ b/docs/zh-Hans/Specifications.md @@ -0,0 +1,3 @@ +## 规约 + +TODO.. \ No newline at end of file diff --git a/docs/zh-Hans/Startup-Templates/Application.md b/docs/zh-Hans/Startup-Templates/Application.md index 31453ff728..4e2c3f6700 100644 --- a/docs/zh-Hans/Startup-Templates/Application.md +++ b/docs/zh-Hans/Startup-Templates/Application.md @@ -348,7 +348,7 @@ Home模块是一个可延迟加载的模块, 它加载应用程序的根地址. React 本机应用程序是用 [Expo](https://expo.io/)生成的. Expo 是一套基于 React Native 构建的工具, 帮助你快速启动一个应用程序, 尽管它有很多功能. -React Native 应用文件夹结构, 如下图所示: +React Native 应用文件夹结构, 如下图所示: ![react-native-folder-structure](../images/react-native-folder-structure.png) @@ -380,7 +380,7 @@ Screens 是通过在 `src/screens` 文件夹中创建将名称分开的文件夹 [Redux](https://redux.js.org/) 被用作状态管理库. [Redux Toolkit](https://redux-toolkit.js.org/) 库被用作高效Redux开发的工具集. -在 `src/store` 文件夹中创建 Actions, reducers, sagas, selectors. 存储文件夹如下: +在 `src/store` 文件夹中创建 Actions, reducers, sagas, selectors. 存储文件夹如下: ![react-native-store-folder](../images/react-native-store-folder.png) diff --git a/docs/zh-Hans/Startup-Templates/Index.md b/docs/zh-Hans/Startup-Templates/Index.md index 57389e6df0..160a2b3e20 100644 --- a/docs/zh-Hans/Startup-Templates/Index.md +++ b/docs/zh-Hans/Startup-Templates/Index.md @@ -2,7 +2,7 @@ 虽然你可以从一个空项目开始并手动添加所需的包,但启动模板可以非常轻松,舒适地使用ABP框架启动新的解决方案. -单击下面列表中的名称以查看相关启动模板的文档: +单击下面列表中的名称以查看相关启动模板的文档: * [**app**](Application.md): 应用程序模板. * [**module**](Module.md): 模块/服务模板. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md index e1cbb73cc1..7c2db90997 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md @@ -33,7 +33,7 @@ - `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象. - `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. -在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: +在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: ````C# using System; @@ -132,7 +132,7 @@ PM> Update-Database #### 添加示例数据 -`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们: +`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们: ![bookstore-books-table](images/bookstore-books-table.png) @@ -290,7 +290,7 @@ namespace Acme.BookStore #### Swagger UI -启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用您自己的端口替换XXXX)作为URL. +启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL. 你会看到一些内置的接口和`Book`的接口,它们都是REST风格的: @@ -380,7 +380,7 @@ context.Menu.AddItem( ![bookstore-localization-files](images/bookstore-localization-files-v2.png) -打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾: +打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾: ````json { diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md index a5e51134fa..e072e5a1a7 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md @@ -14,7 +14,7 @@ ### 解决方案中的测试项目 -解决方案中有多个测试项目: +解决方案中有多个测试项目: ![bookstore-test-projects-v2](images/bookstore-test-projects-v2.png) @@ -68,7 +68,7 @@ namespace Acme.BookStore ```` * 注入`IRepository`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. -* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息). +* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息). ### 测试 BookAppService diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png deleted file mode 100644 index a7d49a661b..0000000000 Binary files a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png deleted file mode 100644 index f531e6f457..0000000000 Binary files a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png deleted file mode 100644 index 7cc96c8c94..0000000000 Binary files a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png deleted file mode 100644 index 79314dd2dc..0000000000 Binary files a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png deleted file mode 100644 index dfd4b5d8aa..0000000000 Binary files a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png deleted file mode 100644 index edf2826361..0000000000 Binary files a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png deleted file mode 100644 index 437c772503..0000000000 Binary files a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md new file mode 100644 index 0000000000..43590faf2b --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-1.md @@ -0,0 +1,1078 @@ +## ASP.NET Core {{UI_Value}} 教程 - 第一章 +````json +//[doc-params] +{ + "UI": ["MVC","NG"] +} +```` +{{ +if UI == "MVC" + DB="ef" + DB_Text="Entity Framework Core" + UI_Text="mvc" +else if UI == "NG" + DB="mongodb" + DB_Text="MongoDB" + UI_Text="angular" +else + DB ="?" + UI_Text="?" +end +}} + +### 关于本教程 + +在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的应用程序. **{{DB_Text}}**将用作ORM提供者,前端使用{{UI_Value}} 和 JavaScript. + +ASP.NET Core {{UI_Value}} 系列教程包括三个3个部分: + +- **Part-1: 创建项目和书籍列表页面(本章)** +- [Part-2: 创建,编辑,删除书籍](Part-2.md) +- [Part-3: 集成测试](Part-3.md) + +> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). + +### 创建新项目 + +创建一个名为 `Acme.BookStore` 的新项目,其中 `Acme` 是公司名 `BookStore` 是项目名. 你可以参阅[入门](../Getting-Started?UI={{UI}}#run-the-application) 文档了解如何创建新项目. 我们将使用CLI创建新项目. + +#### 创建项目 + +使用以下命令创建一个新的ABP项目,使用 `{{DB_Text}}` 做为数据库提供者, UI选项使用 `{{UI_Value}}`. 其他CLI选项请参考[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)文档. + +```bash +abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} --mobile none +``` + +![Creating project](./images/bookstore-create-project-{{UI_Text}}.png) + +### 应用迁移 + +项目创建后,需要应用初始化迁移创建数据库. 运行 `Acme.BookStore.DbMigrator` 应用程序. 它会应用所有迁移,完成流程后你会看到以下结果,数据库已经准备好了! + +![Migrations applied](./images/bookstore-migrations-applied-{{UI_Text}}.png) + +> 另外你也可以在 Visual Studio 包管理控制台运行 `Update-Database` 命令应用迁移. + +#### 初始化数据库表 + +![Initial database tables](./images/bookstore-database-tables-{{DB}}.png) + +### 运行应用程序 + +右键单击{{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} 项目**设置为启动项**. 使用 **CTRL+F5** 或 **F5** 运行应用程序. {{if UI == "NG"}}你会看到BookStore API的Swagger UI.{{end}} + +更多信息,参阅[入门教程](../../Getting-Started?UI={{UI}})的运行应用程序部分. + +![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png) + +{{if UI == "NG"}} + +在 `angular` 下打开命令行终端,执行 `yarn` 命令: + +```bash +yarn +``` + +所有的模块加载后,执行 `yarn start` 命令: + +```bash +yarn start +``` + +默认网站从以下URL访问: + +http://localhost:4200/ + +如果你成功看到登录页面,可以按 `ctrl-c` 退出Angular托管.(我们稍后再运行). + +> 注意, Firefox不使用Windows凭据存储,你需要手动将自签名的开发人员证书导入到Firefox. 打开Firefox并导航到以下网址: +> +> https://localhost:44322/api/abp/application-configuration +> +> 如果你看到下图,单击 **Accept the Risk 和 Continue** 按钮绕过警告. +> +> ![Set as startup project](./images/mozilla-self-signed-cert-error.png) + +{{end}} + +默认的登录凭证: + +* **Username**: admin +* **Password**: 1q2w3E* + +### 解决方案的结构 + +下面的图片展示了从启动模板创建的项目是如何分层的. + +![bookstore-visual-studio-solution](./images/bookstore-solution-structure-{{UI_Text}}.png) + +> 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构. + +### 创建Book实体 + +启动模板中的域层分为两个项目: + + - `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象. + - `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. + +在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: + +````C# +using System; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Acme.BookStore +{ + public class Book : AuditedAggregateRoot + { + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + + protected Book() + { + } + public Book(Guid id, string name, BookType type, DateTime publishDate, float price) + :base(id) + { + Name = name; + Type = type; + PublishDate = publishDate; + Price = price; + } + } +} +```` + +* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](https://docs.abp.io/zh-Hans/abp/latest/Entities). +* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). +* `Guid`是`Book`实体的主键类型. +* 使用 **数据注解** 为EF Core添加映射.或者你也可以使用 EF Core 自带的[fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling). + +#### BookType枚举 + +上面所用到的`BookType`枚举定义如下: + +````C# +namespace Acme.BookStore +{ + public enum BookType + { + Undefined, + Adventure, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry + } +} +```` + +#### 将Book实体添加到DbContext中 + +{{if DB == "ef"}} + +EF Core需要你将实体和 `DbContext` 建立关联.最简单的做法是在`Acme.BookStore.EntityFrameworkCore`项目的`BookStoreDbContext`类中添加`DbSet`属性.如下所示: + +````C# +public class BookStoreDbContext : AbpDbContext +{ + public DbSet Books { get; set; } + ... +} +```` + +{{end}} + +{{if DB == "mongodb"}} + +添加 `IMongoCollection Books` 属性到 `Acme.BookStore.MongoDB` 项目的 `BookStoreMongoDbContext` 中. + +```csharp +public class BookStoreMongoDbContext : AbpMongoDbContext +{ + public IMongoCollection Users => Collection(); + public IMongoCollection Books => Collection();//<--added this line--> + //... +} +``` + +{{end}} + +{{if DB == "ef"}} + +#### 配置你的Book实体 + +在 `Acme.BookStore.EntityFrameworkCore` 项目中打开 `BookStoreDbContextModelCreatingExtensions.cs` 文件,并将以下代码添加到 `ConfigureBookStore` 方法的末尾以配置Book实体: + +````csharp +builder.Entity(b => +{ + b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); + b.ConfigureByConvention(); //auto configure for the base class props + b.Property(x => x.Name).IsRequired().HasMaxLength(128); +}); +```` + +添加 `using Volo.Abp.EntityFrameworkCore.Modeling;` 以使用 `ConfigureByConvention` 扩展方法. + +{{end}} + +{{if DB == "mongodb"}} + +#### 添加种子数据 + +添加种子数据是可选的,但第一次运行时最好将初始数据添加到数据库中. ABP提供了[数据种子系统](https://docs.abp.io/en/abp/latest/Data-Seeding). 在 `*.Domain` 项目下创建派生 `IDataSeedContributor` 的类: + +```csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; + +namespace Acme.BookStore +{ + public class BookStoreDataSeederContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public BookStoreDataSeederContributor( + IRepository bookRepository, + IGuidGenerator guidGenerator) + { + _bookRepository = bookRepository; + _guidGenerator = guidGenerator; + } + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + await _bookRepository.InsertAsync( + new Book( + id: _guidGenerator.Create(), + name: "1984", + type: BookType.Dystopia, + publishDate: new DateTime(1949, 6, 8), + price: 19.84f + ) + ); + + await _bookRepository.InsertAsync( + new Book( + id: _guidGenerator.Create(), + name: "The Hitchhiker's Guide to the Galaxy", + type: BookType.ScienceFiction, + publishDate: new DateTime(1995, 9, 27), + price: 42.0f + ) + ); + } + } +} +``` + +{{end}} + +{{if DB == "ef"}} + +#### 添加新的Migration并更新数据库 + +这个启动模板使用了[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)来创建并维护数据库结构.打开 **程序包管理器控制台(Package Manager Console) (PMC)** (工具/Nuget包管理器菜单) + +![Open Package Manager Console](./images/bookstore-open-package-manager-console.png) + +选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations`作为默认的项目然后执行下面的命令: + +```bash +Add-Migration "Created_Book_Entity" +``` + +![bookstore-pmc-add-book-migration](./images/bookstore-pmc-add-book-migration-v2.png) + +这样就会在 `Migrations` 文件夹中创建一个新的migration类.然后执行 `Update-Database` 命令更新数据库结构: + +````bash +Update-Database +```` + +![bookstore-update-database-after-book-entity](./images/bookstore-update-database-after-book-entity.png) + +#### 添加示例数据 + +`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们: + +```mssql +INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +('f3c04764-6bfd-49e2-859e-3f9bfda6183e', '2018-07-01', '1984',3,'1949-06-08','19.84') + +INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +('13024066-35c9-473c-997b-83cd8d3e29dc', '2018-07-01', 'The Hitchhiker`s Guide to the Galaxy',7,'1995-09-27','42') + +INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +('4fa024a1-95ac-49c6-a709-6af9e4d54b54', '2018-07-02', 'Pet Sematary',5,'1983-11-14','23.7') +``` + +![bookstore-books-table](./images/bookstore-books-table.png) + +{{end}} + +### 创建应用服务 + +下一步是创建[应用服务](../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目: + +* `Acme.BookStore.Application.Contracts`主要包含你的DTO和应用程序服务接口. +* `Acme.BookStore.Application`包含应用程序服务的实现. + +#### BookDto + +在`Acme.BookStore.Application.Contracts`项目中创建一个名为`BookDto`的DTO类: + +````C# +using System; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore +{ + public class BookDto : AuditedEntityDto + { + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + } +} +```` + +* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects)查看更多信息. +* 为了在页面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层. +* `BookDto`继承自 `AuditedEntityDto`.跟上面定义的`Book`类一样具有一些审计属性. + +在将书籍返回到表示层时,需要将`Book`实体转换为`BookDto`对象. [AutoMapper](https://automapper.org)库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在`Acme.BookStore.Application`项目的`BookStoreApplicationAutoMapperProfile`类中定义映射: + +````csharp +using AutoMapper; + +namespace Acme.BookStore +{ + public class BookStoreApplicationAutoMapperProfile : Profile + { + public BookStoreApplicationAutoMapperProfile() + { + CreateMap(); + } + } +} +```` + +#### CreateUpdateBookDto + +在`Acme.BookStore.Application.Contracts`项目中创建一个名为`CreateUpdateBookDto`的DTO类: + +````c# +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.AutoMapper; + +namespace Acme.BookStore +{ + public class CreateUpdateBookDto + { + [Required] + [StringLength(128)] + public string Name { get; set; } + + [Required] + public BookType Type { get; set; } = BookType.Undefined; + + [Required] + public DateTime PublishDate { get; set; } + + [Required] + public float Price { get; set; } + } +} +```` + +* 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息. +* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation). + +就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射: + +````csharp +using AutoMapper; + +namespace Acme.BookStore +{ + public class BookStoreApplicationAutoMapperProfile : Profile + { + public BookStoreApplicationAutoMapperProfile() + { + CreateMap(); + CreateMap(); //<--added this line--> + } + } +} +```` + +#### IBookAppService + +在`Acme.BookStore.Application.Contracts`项目中定义一个名为`IBookAppService`的接口: + +````C# +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Acme.BookStore +{ + public interface IBookAppService : + ICrudAppService< //定义了CRUD方法 + BookDto, //用来展示书籍 + Guid, //Book实体的主键 + PagedAndSortedResultRequestDto, //获取书籍的时候用于分页和排序 + CreateUpdateBookDto, //用于创建书籍 + CreateUpdateBookDto> //用于更新书籍 + { + + } +} +```` + +* 框架定义应用程序服务的接口**不是必需的**. 但是,它被建议作为最佳实践. +* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 你可以从空的`IApplicationService`接口继承并手动定义自己的方法. +* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定. + +#### BookAppService + +在`Acme.BookStore.Application`项目中实现名为`BookAppService`的`IBookAppService`: + +````C# +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore +{ + public class BookAppService : + CrudAppService, + IBookAppService + { + public BookAppService(IRepository repository) + : base(repository) + { + + } + } +} +```` + +* `BookAppService`继承了`CrudAppService<...>`.它实现了上面定义的CRUD方法. +* `BookAppService`注入`IRepository `,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](https://docs.abp.io/zh-Hans/abp/latest/Repositories) +* `BookAppService`使用`IObjectMapper`将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作. + +### 自动生成API Controllers + +你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](https://docs.abp.io/zh-Hans/abp/latest/API/Auto-API-Controllers)按照惯例将你的应用程序服务配置为MVC API控制器. + +#### Swagger UI + +启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL. + +你会看到一些内置的接口和`Book`的接口,它们都是REST风格的: + +![bookstore-swagger](images/bookstore-swagger.png) + +Swagger有一个很好的UI来测试API. 你可以尝试执行`[GET] /api/app/book` API来获取书籍列表. + +{{if UI == "MVC"}} + +### 动态JavaScript代理 + +在Javascript端通过AJAX的方式调用HTTP API接口是很常见的,你可以使用`$.ajax`或者其他的工具来调用接口.当然,ABP中提供了更好的方式. + +ABP **自动** 为所有的API接口创建了JavaScript **代理**.因此,你可以像调用 **JavaScript function**一样调用任何接口. + +#### 在浏览器的开发者控制台中测试接口 + +你可以使用你钟爱的浏览器的 **开发者控制台** 中轻松测试JavaScript代理.运行程序,并打开浏览器的 **开发者工具**(快捷键:F12),切换到 **Console** 标签,输入下面的代码并回车: + +````js +acme.bookStore.book.getList({}).done(function (result) { console.log(result); }); +```` + +* `acme.bookStore`是`BookAppService`的命名空间,转换成了[驼峰命名](https://en.wikipedia.org/wiki/Camel_case). +* `book`是`BookAppService`转换后的名字(去除了AppService后缀并转成了驼峰命名). +* `getList`是定义在`AsyncCrudAppService`基类中的`GetListAsync`方法转换后的名字(去除了Async后缀并转成了驼峰命名). +* `{}`参数用于将空对象发送到`GetListAsync`方法,该方法通常需要一个类型为`PagedAndSortedResultRequestDto`的对象,用于向服务器发送分页和排序选项(所有属性都是可选的,所以你可以发送一个空对象). +* `getList`方法返回了一个`promise`.因此,你可以传递一个回调函数到`done`(或者`then`)方法中来获取服务返回的结果. + +运行这段代码会产生下面的输出: + +![bookstore-test-js-proxy-getlist](./images/bookstore-test-js-proxy-getlist.png) + +你可以看到服务器返回的 **book list**.你还可以切换到开发者工具的 **network** 查看客户端到服务器端的通讯信息: + +![bookstore-test-js-proxy-getlist-network](./images/bookstore-test-js-proxy-getlist-network.png) + +我们使用`create`方法 **创建一本新书**: + +````js +acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); }); +```` + +你会看到控制台会显示类似这样的输出: + +````text +successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 +```` + +检查数据库中的`Books`表以查看新书. 你可以自己尝试`get`,`update`和`delete`功能. + +### 创建书籍页面 + +现在我们来创建一些可见和可用的东西,取代经典的MVC,我们使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start). + +在 `Acme.BookStore.Web`项目的`Pages`文件夹下创建一个新的文件夹叫`Books`并添加一个名为`Index.cshtml`的Razor Page. + +![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png) + +打开`Index.cshtml`并把内容修改成下面这样: + +**Index.cshtml:** + +````html +@page +@using Acme.BookStore.Web.Pages.Books +@inherits Acme.BookStore.Web.Pages.BookStorePage +@model IndexModel + +

Books

+```` + +* 此代码更改了Razor View Page Model的默认继承,因此它从`BookStorePage`类(而不是`PageModel`)继承.启动模板附带的`BookStorePage`类,提供所有页面使用的一些共享属性/方法. +* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Books`命名空间,或者在`Index.cshtml`中更新它. + +**Index.cshtml.cs:** + +```csharp +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} +``` + +#### 将Books页面添加到主菜单 + +打开`Menus`文件夹中的 `BookStoreMenuContributor` 类,在`ConfigureMainMenuAsync`方法的底部添加如下代码: + +````csharp +//... +namespace Acme.BookStore.Web.Menus +{ + public class BookStoreMenuContributor : IMenuContributor + { + private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + //<-- added the below code + context.Menu.AddItem( + new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) + .AddItem( + new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books") + ) + ); + //--> + } + } +} +```` + +{{end}} + +#### 本地化菜单 + +本地化文本位于`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下: + +![bookstore-localization-files](./images/bookstore-localization-files-v2.png) + +打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾: + +````json +{ + "Culture": "en", + "Texts": { + "Menu:Home": "Home", + "Welcome": "Welcome", + "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", + + "Menu:BookStore": "Book Store", + "Menu:Books": "Books", + "Actions": "Actions", + "Edit": "Edit", + "PublishDate": "Publish date", + "NewBook": "New book", + "Name": "Name", + "Type": "Type", + "Price": "Price", + "CreationTime": "Creation time", + "AreYouSureToDelete": "Are you sure you want to delete this item?" + } +} +```` + +* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](https://docs.abp.io/zh-Hans/abp/latest/Localization). +* 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加`Menu:`前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将**返回**到本地化的key(ASP.NET Core的标准行为). + +运行该应用程序,看到新菜单项已添加到顶部栏: + +![bookstore-menu-items](./images/bookstore-new-menu-item.png) + +点击BookStore下Books子菜单项就会跳转到新增的书籍页面. + +#### 书籍列表 + +我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示页面上的表格列表. [Datatables](https://datatables.net/)可以完全通过AJAX工作,速度快,并提供良好的用户体验. Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,而需要在页面中引用样式和脚本文件. + +##### Index.cshtml + +将`Pages/Books/Index.cshtml`改成下面的样子: + +````html +@page +@inherits Acme.BookStore.Web.Pages.BookStorePage +@model Acme.BookStore.Web.Pages.Books.IndexModel +@section scripts +{ + +} + + +

@L["Books"]

+
+ + + + + @L["Name"] + @L["Type"] + @L["PublishDate"] + @L["Price"] + @L["CreationTime"] + + + + +
+```` + +* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Bundling-Minification)获取更多信息. +* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Tag-Helpers/Index). +* 你可以像上面本地化菜单一样 **本地化** 列名. + +#### 添加脚本文件 + +在`Pages/Books/`文件夹中创建 `index.js`文件 + +![bookstore-index-js-file](./images/bookstore-index-js-file-v2.png) + +`index.js`的内容如下: + +````js +$(function () { + var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), + columnDefs: [ + { data: "name" }, + { data: "type" }, + { data: "publishDate" }, + { data: "price" }, + { data: "creationTime" } + ] + })); +}); +```` + +* `abp.libs.datatables.createAjax`是帮助ABP的动态JavaScript API代理跟[Datatable](https://datatables.net/)的格式相适应的辅助方法. +* `abp.libs.datatables.normalizeConfiguration`是另一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置. +* `acme.bookStore.book.getList`是获取书籍列表的方法(上面已经介绍过了) +* 查看 [Datatable文档](https://datatables.net/manual/) 了解更多配置项. + +最终的页面如下: + +![Book list](./images/bookstore-book-list-2.png) + +{{end}} + +{{if UI == "NG"}} + +### Angular 开发 + +#### 创建books页面 + +是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具: + +- [Angular CLI](https://angular.io/cli) 用于创建模块,组件和服务. +- [NGXS](https://ngxs.gitbook.io/ngxs/) 用于管理状态库. +- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库. +- [Visual Studio Code](https://code.visualstudio.com/) 用做代码编辑器 (你可以选择自己喜欢的编辑器). + +#### 安装 NPM 包 + +在 `angular` 目录下打开命令行窗口,选择 `yarn` 命令安装NPM包: + +```bash +yarn +``` + +#### BooksModule + +运行以下命令创建一个名为 `BooksModule` 的新模块: + +```bash +yarn ng generate module books --route books --module app.module +``` + +![Generating books module](./images/bookstore-creating-books-module-terminal.png) + +#### 路由 + +打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的 `import` 和替换 `books` 路径: + +```js +import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <== + +//...replaced original books path with the below +{ + path: 'books', + component: ApplicationLayoutComponent, + loadChildren: () => import('./books/books.module').then(m => m.BooksModule), + data: { + routes: { + name: '::Menu:Books', + iconClass: 'fas fa-book' + } as ABP.Route + }, +} +``` + +* `ApplicationLayoutComponent` 配置将应用程序布局设置为新页面, 我们添加了 `data` 对象. `name` 是菜单项的名称,`iconClass` 是菜单项的图标. + +运行 `yarn start` 等待Angular为应用程序启动服务: + +```bash +yarn start +``` + +打开浏览器导航到 http://localhost:4200/books. 你会看到一个带有 "*books works!*" 的空白页. + +![initial-books-page](./images/bookstore-initial-books-page-with-layout.png) + +#### Book 列表组件 + +用以下内容替换 `books.component.html`: + +```html + +``` + +在命令行运行以下命令,生成名为 book-list 的新组件: + +```bash +yarn ng generate component books/book-list +``` + +![Creating books list](./images/bookstore-creating-book-list-terminal.png) + +打开 `app\books` 目录下的 `books.module.ts` 文件,使用以下内容替换它: + +```js +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BooksRoutingModule } from './books-routing.module'; +import { BooksComponent } from './books.component'; +import { BookListComponent } from './book-list/book-list.component'; +import { SharedModule } from '../shared/shared.module'; //<== added this line ==> + +@NgModule({ + declarations: [BooksComponent, BookListComponent], + imports: [ + CommonModule, + BooksRoutingModule, + SharedModule, //<== added this line ==> + ] +}) +export class BooksModule { } +``` + +* 我们导入了 `SharedModule` 并添加到 `imports` 数组. + +打开 `app\books` 目录下的 `books-routing.module.ts` 文件用以下内容替换它: + +```js +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { BooksComponent } from './books.component'; +import { BookListComponent } from './book-list/book-list.component'; //<== added this line ==> + +//<== replaced routes ==> +const routes: Routes = [ + { + path: '', + component: BooksComponent, + children: [{ path: '', component: BookListComponent }], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class BooksRoutingModule { } +``` + +* 我们导入了 `BookListComponent` 并替换 `routes` 常量. + +我们将看到books页面的 **book-list works!**: + +![Initial book list page](./images/bookstore-initial-book-list-page.png) + +#### 创建 BooksState + +运行以下命令创建名为 `BooksState` 的新state: + +```bash +npx @ngxs/cli --name books --directory src/app/books +``` + +* 此命令在 `src/app/books/state` 文件夹下创建了 `books.state.ts` 和 `books.actions.ts` 文件. 参阅 [NGXS CLI文档](https://www.ngxs.io/plugins/cli)了解更多. + +将 `BooksState` 导入到 `src/app` 文件夹中的 `app.module.ts` 中. 然后添加 `BooksState` 到 `NgxsModule` 的 `forRoot` 静态方法,作为该方法的第一个参数的数组元素. + +```js +// ... +import { BooksState } from './books/state/books.state'; //<== imported BooksState ==> + +@NgModule({ + imports: [ + // other imports + + NgxsModule.forRoot([BooksState]), //<== added BooksState ==> + + //other imports + ], + // ... +}) +export class AppModule {} +``` + +#### 生成代理 + +ABP CLI提供了 `generate-proxy` 命令为你的服务HTTP API生成客户端代理简化客户端使用服务的成本. 运行 `generate-proxy` 命令前你的host必须正在运行. 参阅 [CLI 文档](../CLI.md). + +在 `angular` 文件夹下运行以下命令: + +```bash +abp generate-proxy --module app +``` + +![Generate proxy command](./images/generate-proxy-command.png) + +生成的文件如下: + +![Generated files](./images/generated-proxies.png) + +#### GetBooks 动作 + +动作可以被认为是一个命令,它应该触发某些事情发生,或者是已经发生的事情的结果事件.[See NGXS Actions文档](https://www.ngxs.io/concepts/actions). + +打开 `app/books/state` 目录下的 `books.actions.ts` 文件用以下内容替换它: + +```js +export class GetBooks { + static readonly type = '[Books] Get'; +} +``` + +#### 实现 BooksState + +打开 `app/books/state` 目录下的 `books.state.ts` 文件用以下内容替换它: + +```js +import { PagedResultDto } from '@abp/ng.core'; +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { GetBooks } from './books.actions'; +import { BookService } from '../../app/shared/services'; +import { tap } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; +import { BookDto } from '../../app/shared/models'; + +export class BooksStateModel { + public book: PagedResultDto; +} + +@State({ + name: 'BooksState', + defaults: { book: {} } as BooksStateModel, +}) +@Injectable() +export class BooksState { + @Selector() + static getBooks(state: BooksStateModel) { + return state.book.items || []; + } + + constructor(private bookService: BookService) {} + + @Action(GetBooks) + get(ctx: StateContext) { + return this.bookService.getListByInput().pipe( + tap((booksResponse) => { + ctx.patchState({ + book: booksResponse, + }); + }) + ); + } +} +``` + +* 我们添加了book属性到BooksStateModel模态框. +* 我们添加了 `GetBooks` 动作. 它通过 ABP CLI生成的 `BooksService` 检索图书数据. +* `NGXS` 需要在不订阅get函数的情况下返回被观察对象. + +#### BookListComponent + +打开 `app\books\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 '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = BookType; + + loading = false; + + constructor(private store: Store) {} + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); + } +} +``` + +* 我们添加了 `get` 函数获取books更新store. +* 有关 `NGXS` 特性的更多信息请参见NGXS文档中的[Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions)和[Select](https://ngxs.gitbook.io/ngxs/concepts/select). + +打开 `app\books\book-list` 目录下的 `book-list.component.html` 用以下内容替换它: + +```html +
+
+
+
+
+ {%{{{ "::Menu:Books" | abpLocalization }}}%} +
+
+
+
+
+
+ + + + + {%{{{ "::Name" | abpLocalization }}}%} + {%{{{ "::Type" | abpLocalization }}}%} + {%{{{ "::PublishDate" | abpLocalization }}}%} + {%{{{ "::Price" | abpLocalization }}}%} + + + + + {%{{{ data.name }}}%} + {%{{{ booksType[data.type] }}}%} + {%{{{ data.publishDate | date }}}%} + {%{{{ data.price }}}%} + + +
+
+``` + +* 我们添加了图书列表页面的HTML代码. + +现在你可以在浏览器看到最终结果: + +![Book list final result](./images/bookstore-book-list.png) + +项目的文件系统结构: + +![Book list final result](./images/bookstore-angular-file-tree.png) + +在本教程中我们遵循了官方的[Angular风格指南](https://angular.io/guide/styleguide#file-tree). + +{{end}} + +### 下一章 + +参阅[第二章](part-2.md)了解创建,更新和删除图书. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md new file mode 100644 index 0000000000..56dadcbbcd --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-2.md @@ -0,0 +1,1349 @@ +## ASP.NET Core {{UI_Value}} 教程 - 第二章 +````json +//[doc-params] +{ + "UI": ["MVC","NG"] +} +```` + +{{ +if UI == "MVC" + DB="ef" + DB_Text="Entity Framework Core" + UI_Text="mvc" +else if UI == "NG" + DB="mongodb" + DB_Text="MongoDB" + UI_Text="angular" +else + DB ="?" + UI_Text="?" +end +}} + +### 关于本教程 + +这是ASP.NET Core{{UI_Value}}系列教程的第二章. 共有三章: + +- [Part-1: 创建项目和书籍列表页面](Part-1.md) +- **Part 2: 创建,编辑,删除书籍(本章)** +- [Part-3: 集成测试](Part-3.md) + +> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). + +{{if UI == "MVC"}} + +### 新增 Book 实体 + +通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: + +![bookstore-create-dialog](./images/bookstore-create-dialog-2.png) + +#### 新建 modal form + +在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: + +![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png) + +##### CreateModal.cshtml.cs + +打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现: + +````C# +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class CreateModalModel : BookStorePageModel + { + [BindProperty] + public CreateUpdateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync(Book); + return NoContent(); + } + } +} +```` + +* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法. +* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上. +* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法. + +##### CreateModal.cshtml + +打开 `CreateModal.cshtml` 文件并粘贴如下代码: + +````html +@page +@inherits Acme.BookStore.Web.Pages.BookStorePage +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Acme.BookStore.Web.Pages.Books.CreateModalModel +@{ + Layout = null; +} + + + + + + + + + +```` + +* 这个 modal 使用 `abp-dynamic-form` Tag Helper 根据 `CreateBookViewModel` 类自动构建了表单. + * `abp-model` 指定了 `Book` 属性为模型对象. + * `data-ajaxForm` 设置了表单通过AJAX提交,而不是经典的页面回发. + * `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要). + +#### 添加 "New book" 按钮 + +打开 `Pages/Books/Index.cshtml` 并按如下代码修改 `abp-card-header` : + +````html + + + +

@L["Books"]

+
+ + + +
+
+```` + +如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: + +![bookstore-new-book-button](./images/bookstore-new-book-button.png) + +打开 `Pages/books/index.js` 在 `datatable` 配置代码后面添加如下代码: + +````js +var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + +createModal.onResult(function () { + dataTable.ajax.reload(); +}); + +$('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); +}); +```` + +* `abp.ModalManager` 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节. + +现在,你可以 **运行程序** 通过新的 modal form 来创建书籍了. + +### 编辑更新已存在的 Book 实体 + +在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: + +![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png) + +#### EditModal.cshtml.cs + +展开 `EditModal.cshtml`,打开 `EditModal.cshtml.cs` 文件( `EditModalModel` 类) 并替换成以下代码: + +````csharp +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class EditModalModel : BookStorePageModel + { + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public CreateUpdateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public EditModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync() + { + var bookDto = await _bookAppService.GetAsync(Id); + Book = ObjectMapper.Map(bookDto); + } + + public async Task OnPostAsync() + { + await _bookAppService.UpdateAsync(Id, Book); + return NoContent(); + } + } +} +```` + +* `[HiddenInput]` 和 `[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串中获取Id的值. +* 在 `OnGetAsync` 方法中,将 `BookAppService.GetAsync` 方法返回的 `BookDto` 映射成 `CreateUpdateBookDto` 并赋值给Book属性. +* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体. + +#### BookDto到CreateUpdateBookDto对象映射 + +为了执行`BookDto`到`CreateUpdateBookDto`对象映射,请打开`Acme.BookStore.Web`项目中的`BookStoreWebAutoMapperProfile.cs`并更改它,如下所示: + +````csharp +using AutoMapper; + +namespace Acme.BookStore.Web +{ + public class BookStoreWebAutoMapperProfile : Profile + { + public BookStoreWebAutoMapperProfile() + { + CreateMap(); + } + } +} +```` + +* 刚刚添加了`CreateMap();`作为映射定义. + +#### EditModal.cshtml + +将 `EditModal.cshtml` 页面内容替换成如下代码: + +````html +@page +@inherits Acme.BookStore.Web.Pages.BookStorePage +@using Acme.BookStore.Web.Pages.Books +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model EditModalModel +@{ + Layout = null; +} + + + + + + + + + + +```` + +这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点: + +* 它包含`id`属性的`abp-input`, 用于存储编辑书的 `id` (它是隐藏的Input) +* 此页面指定的post地址是`Books/EditModal`, 并用文本 *Update* 作为 modal 标题. + +#### 为表格添加 "操作(Actions)" 下拉菜单 + +我们将为表格每行添加下拉按钮 ("Actions") . 最终效果如下: + +![bookstore-books-table-actions](images/bookstore-books-table-actions.png) + +打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码: + +````html + + + + @L["Actions"] + @L["Name"] + @L["Type"] + @L["PublishDate"] + @L["Price"] + @L["CreationTime"] + + + +```` + +* 只是为"Actions"增加了一个 `th` 标签. + +打开 `Pages/books/index.js` 并用以下内容进行替换: + +````js +$(function () { + + var l = abp.localization.getResource('BookStore'); + + var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); + + var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + processing: true, + serverSide: true, + paging: true, + searching: false, + autoWidth: false, + scrollCollapse: true, + order: [[1, "asc"]], + ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), + columnDefs: [ + { + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + } + ] + } + }, + { data: "name" }, + { data: "type" }, + { data: "publishDate" }, + { data: "price" }, + { data: "creationTime" } + ] + })); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +* 通过 `abp.localization.getResource('BookStore')` 可以在客户端使用服务器端定义的相同的本地化语言文本. +* 添加了一个名为 `createModal` 的新的 `ModalManager` 来打开创建用的 modal 对话框. +* 添加了一个名为 `editModal` 的新的 `ModalManager` 来打开编辑用的 modal 对话框. +* 在 `columnDefs` 起始处新增一列用于显示 "Actions" 下拉按钮. +* "New Book"动作只需调用`createModal.open`来打开创建对话框. +* "Edit" 操作只是简单调用 `editModal.open` 来打开编辑对话框. + +现在,你可以运行程序,通过编辑操作来更新任一个book实体. + +### 删除一个已有的Book实体 + +打开 `Pages/books/index.js` 文件,在 `rowAction` `items` 下新增一项: + +````js +{ + text: l('Delete'), + confirmMessage: function (data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function (data) { + acme.bookStore.book + .delete(data.record.id) + .then(function() { + abp.notify.info(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } +} +```` + +* `confirmMessage` 用来在实际执行 `action` 之前向用户进行确认. +* 通过javascript代理方法 `acme.bookStore.book.delete` 执行一个AJAX请求来删除一个book实体. +* `abp.notify.info` 用来在执行删除操作后显示一个toastr通知信息. + +最终的 `index.js` 文件内容如下所示: + +````js +$(function () { + + var l = abp.localization.getResource('BookStore'); + + var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); + + var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + processing: true, + serverSide: true, + paging: true, + searching: false, + autoWidth: false, + scrollCollapse: true, + order: [[1, "asc"]], + ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), + columnDefs: [ + { + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + confirmMessage: function (data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function (data) { + acme.bookStore.book + .delete(data.record.id) + .then(function() { + abp.notify.info(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } + } + ] + } + }, + { data: "name" }, + { data: "type" }, + { data: "publishDate" }, + { data: "price" }, + { data: "creationTime" } + ] + })); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +打开`Acme.BookStore.Domain.Shared`项目中的`en.json`并添加以下行: + +````json +"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", +"SuccessfullyDeleted": "Successfully deleted" +```` + +运行程序并尝试删除一个book实体. + +{{end}} + +### 新增 Book 实体 + +下面的章节中,你将学习到如何创建一个新的模态对话框来新增Book实体. + +#### 状态定义 + +在 `books\state` 文件夹下打开 `books.action.ts` 文件,使用以下内容替换它: + +```js +import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==> + +export class GetBooks { + static readonly type = '[Books] Get'; +} + +// added CreateUpdateBook class +export class CreateUpdateBook { + static readonly type = '[Books] Create Update Book'; + constructor(public payload: CreateUpdateBookDto) { } +} +``` + +* 我们导入了 `CreateUpdateBookDto` 模型并且创建了 `CreateUpdateBook` 动作. + +打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换它: + +```js +import { PagedResultDto } from '@abp/ng.core'; +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { GetBooks, CreateUpdateBook } from './books.actions'; // <== added CreateUpdateBook==> +import { BookService } from '../../app/shared/services'; +import { tap } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; +import { BookDto } from '../../app/shared/models'; + +export class BooksStateModel { + public book: PagedResultDto; +} + +@State({ + name: 'BooksState', + defaults: { book: {} } as BooksStateModel, +}) +@Injectable() +export class BooksState { + @Selector() + static getBooks(state: BooksStateModel) { + return state.book.items || []; + } + + constructor(private bookService: BookService) {} + + @Action(GetBooks) + get(ctx: StateContext) { + return this.bookService.getListByInput().pipe( + tap((bookResponse) => { + ctx.patchState({ + book: bookResponse, + }); + }) + ); + } + + // added CreateUpdateBook action listener + @Action(CreateUpdateBook) + save(ctx: StateContext, action: CreateUpdateBook) { + return this.bookService.createByInput(action.payload); + } +} +``` + +* 我们导入了 `CreateUpdateBook` 动作并且定义了 `save` 方法监听 `CreateUpdateBook` 动作去创建图书. + +当 `SaveBook` 动作被分派时,save方法被执行. 它调用 `BookService` 的 `createByInput` 方法. + +#### 添加模态到 BookListComponent + +打开 `books\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它: + +```html +
+
+
+
+
+ {%{{{ '::Menu:Books' | abpLocalization }}}%} +
+
+ +
+
+ +
+
+
+
+
+ + + + + {%{{{ "::Name" | abpLocalization }}}%} + {%{{{ "::Type" | abpLocalization }}}%} + {%{{{ "::PublishDate" | abpLocalization }}}%} + {%{{{ "::Price" | abpLocalization }}}%} + + + + + {%{{{ data.name }}}%} + {%{{{ booksType[data.type] }}}%} + {%{{{ data.publishDate | date }}}%} + {%{{{ data.price }}}%} + + +
+
+ + + + +

{%{{{ '::NewBook' | abpLocalization }}}%}

+
+ + + + + + +
+``` + +* 我们添加了 `abp-modal` 渲染模态框,允许用户创建新书. +* `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处. +* 我们添加了 `New book` 按钮到 `AbpContentToolbar`. + +打开 `books\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 '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + 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**按钮看到模态框. + +![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) + +#### 添加响应式表单 + +[响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入. + +打开 `app\books\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 '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==> + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = BookType; + + loading = false; + + isModalOpen = false; + + form: FormGroup; // <== added this line ==> + + constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==> + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); + } + + createBook() { + this.buildForm(); //<== added this line ==> + this.isModalOpen = true; + } + + // added buildForm method + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } +} +``` + +* 我们导入了 `FormGroup, FormBuilder and Validators`. +* 我们添加了 `form: FormGroup` 变量. +* 我们注入了 `fb: FormBuilder` 服务到构造函数. [FormBuilder](https://angular.io/api/forms/FormBuilder) 服务为生成控件提供了方便的方法. 它减少了构建复杂表单所需的样板文件的数量. +* 我们添加了 `buildForm` 方法到文件末尾, 在 `createBook` 方法调用 `buildForm()` 方法. 该方法创建一个响应式表单去创建新书. + * `FormBuilder` 内的 `fb.group` 方法创建一个 `FormGroup`. + * 添加了 `Validators.required` 静态方法用于验证表单元素. + +#### 创建表单的DOM元素 + +打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 ` `: + +```html + + +
+ * + +
+ +
+ * + +
+ +
+ * + +
+ +
+ * + +
+ +
+``` + +- 模板创建了带有 `Name`, `Price`, `Type` 和 `Publish` 时间字段的表单. +- 我们在组件中使用了 [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview). + +#### Datepicker 要求 + +打开 `app\books` 文件夹下的 `books.module.ts` 文件,使用以下内容替换它: + +```js +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BooksRoutingModule } from './books-routing.module'; +import { BooksComponent } from './books.component'; +import { BookListComponent } from './book-list/book-list.component'; +import { SharedModule } from '../shared/shared.module'; +import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==> + +@NgModule({ + declarations: [BooksComponent, BookListComponent], + imports: [ + CommonModule, + BooksRoutingModule, + SharedModule, + NgbDatepickerModule //<== added this line ==> + ] +}) +export class BooksModule { } +``` + +* 我们导入了 `NgbDatepickerModule` 来使用日期选择器. + +打开 `app\books\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 '../../app/shared/models'; +import { GetBooks } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==> + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==> +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = BookType; + + //added bookTypeArr array + bookTypeArr = Object.keys(BookType).filter( + (bookType) => typeof this.booksType[bookType] === 'number' + ); + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + constructor(private store: Store, private fb: FormBuilder) {} + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); + } + + createBook() { + this.buildForm(); + this.isModalOpen = true; + } + + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } +} +``` + +* 我们导入了 ` NgbDateNativeAdapter, NgbDateAdapter` + +* 我们添加了一个新的 `NgbDateAdapter` 提供程序,它将Datepicker值转换为Date类型. 有关更多详细信息,请参见[datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview). + +* 我们添加了 `bookTypeArr` 数组,以便能够在combobox值中使用它. `bookTypeArr` 包含 `BookType` 枚举的字段. 得到的数组如下所示: + + ```js + ['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] + ``` + + 在先前的表单模板中 用 `ngFor` 使用这个数组. + +现在你可以打开浏览器看到以下变化: + +![New book modal](./images/bookstore-new-book-form.png) + +#### 保存图书 + +打开 `app\books\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 '../../app/shared/models'; +import { GetBooks, CreateUpdateBook } from '../state/books.actions'; // <== added CreateUpdateBook ==> +import { BooksState } from '../state/books.state'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = BookType; + + //added bookTypeArr array + bookTypeArr = Object.keys(BookType).filter( + (bookType) => typeof this.booksType[bookType] === 'number' + ); + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + constructor(private store: Store, private fb: FormBuilder) {} + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); + } + + createBook() { + this.buildForm(); + this.isModalOpen = true; + } + + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } + + //<== added save ==> + save() { + if (this.form.invalid) { + return; + } + + this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.get(); + }); + } +} +``` + +* 我们导入了 `CreateUpdateBook`. +* 我们添加了 `save` 方法. + +打开 `app\books\book-list` 文件夹下的 `app\books\book-list`文件, 添加 `abp-button` 保存图书. + +```html + + + + + + +``` + +使用以下内容替换 `
` 标签: + +```html + +``` + +* 我们添加了 `(ngSubmit)="save()"` 到 `` 元素,当按下enter时保存图书. +* 我们在模态框的底部添加了 `abp-button` 来保存图书. + +模态框最终看起来像这样: + +![Save button to the modal](./images/bookstore-new-book-form-v2.png) + +### 更新图书 + +#### CreateUpdateBook 动作 + +打开 `books\state` 文件夹下的 `books.actions.ts` 文件,使用以下内容替换它: + +```js +import { CreateUpdateBookDto } from '../../app/shared/models'; + +export class GetBooks { + static readonly type = '[Books] Get'; +} + +export class CreateUpdateBook { + static readonly type = '[Books] Create Update Book'; + constructor(public payload: CreateUpdateBookDto, public id?: string) { } // <== added id parameter ==> +} +``` + +* 我们在 `CreateUpdateBook` 动作的构造函数添加了 `id` 参数. + +打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换 `save` 方法: + +```js +@Action(CreateUpdateBook) +save(ctx: StateContext, action: CreateUpdateBook) { + if (action.id) { + return this.bookService.updateByIdAndInput(action.payload, action.id); + } else { + return this.bookService.createByInput(action.payload); + } +} +``` + +#### BookListComponent + +打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,在构造函数注入 `BookService` 服务,并添加 名为 `selectedBook` 的变量. + +```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 '../../app/shared/models'; +import { GetBooks, CreateUpdateBook } from '../state/books.actions'; +import { BooksState } from '../state/books.state'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; +import { BookService } from '../../app/shared/services'; // <== imported BookService ==> + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = BookType; + + bookTypeArr = Object.keys(BookType).filter( + (bookType) => typeof this.booksType[bookType] === 'number' + ); + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + selectedBook = {} as BookDto; // <== declared selectedBook ==> + + constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==> + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store + .dispatch(new GetBooks()) + .pipe(finalize(() => (this.loading = false))) + .subscribe(() => {}); + } + + // <== this method is replaced ==> + createBook() { + this.selectedBook = {} as BookDto; // <== added ==> + this.buildForm(); + this.isModalOpen = true; + } + + // <== added editBook method ==> + editBook(id: string) { + this.bookService.getById(id).subscribe((book) => { + this.selectedBook = book; + this.buildForm(); + this.isModalOpen = true; + }); + } + + // <== this method is replaced ==> + buildForm() { + this.form = this.fb.group({ + name: [this.selectedBook.name || '', Validators.required], + type: [this.selectedBook.type || null, Validators.required], + publishDate: [ + this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, + Validators.required, + ], + price: [this.selectedBook.price || null, Validators.required], + }); + } + + save() { + if (this.form.invalid) { + return; + } + + //<== added this.selectedBook.id ==> + this.store + .dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) + .subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.get(); + }); + } +} +``` + +* 我们导入了 `BookService`. +* 我们声明了类型为 `BookDto` 的 `selectedBook` 变量. +* 我们在构造函数注入了 `BookService`, 它用于检索正在编辑的图书数据. +* 我们添加了 `editBook` 方法, 根据给定图书 `Id` 设置 `selectedBook` 对象. +* 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单. +* 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象. +* 我们在 `CreateUpdateBook` 构造函数添加了 `selectedBook.id`. + +#### 添加 "Actions" 下拉框到表格 + +打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `
` 标签: + +```html +
+ + + + + {%{{{ "::Actions" | abpLocalization }}}%} + {%{{{ "::Name" | abpLocalization }}}%} + {%{{{ "::Type" | abpLocalization }}}%} + {%{{{ "::PublishDate" | abpLocalization }}}%} + {%{{{ "::Price" | abpLocalization }}}%} + + + + + +
+ +
+ +
+
+ + {%{{{ data.name }}}%} + {%{{{ booksType[data.type] }}}%} + {%{{{ data.publishDate | date }}}%} + {%{{{ data.price }}}%} + +
+
+``` + +- 我们添加了 "Actions" 栏的 `th`. +- 我们添加了带有 `ngbDropdownToggle` 的 `button`,在点击按钮时打开操作. +- 我们习惯于将[NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples)用于操作的下拉菜单. + +UI最终看起来像这样: + +![Action buttons](./images/bookstore-actions-buttons.png) + +打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `` 标签: + +```html + +

{%{{{ (selectedBook.id ? 'AbpIdentity::Edit' : '::NewBook' ) | abpLocalization }}}%}

+
+``` + +* **Edit** 文本做为编辑记录操作的标题, **New Book** 做为添加记录操作的标题. + +### 删除图书 + +#### DeleteBook 动作 + +打开 `books\state` 文件夹下的 `books.actions.ts` 文件添加名为 `DeleteBook` 的动作. + +```js +export class DeleteBook { + static readonly type = '[Books] Delete'; + constructor(public id: string) {} +} +``` + +打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换它: + +```js +import { PagedResultDto } from '@abp/ng.core'; +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { GetBooks, CreateUpdateBook, DeleteBook } from './books.actions'; // <== added DeleteBook==> +import { BookService } from '../../app/shared/services'; +import { tap } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; +import { BookDto } from '../../app/shared/models'; + +export class BooksStateModel { + public book: PagedResultDto; +} + +@State({ + name: 'BooksState', + defaults: { book: {} } as BooksStateModel, +}) +@Injectable() +export class BooksState { + @Selector() + static getBooks(state: BooksStateModel) { + return state.book.items || []; + } + + constructor(private bookService: BookService) {} + + @Action(GetBooks) + get(ctx: StateContext) { + return this.bookService.getListByInput().pipe( + tap((booksResponse) => { + ctx.patchState({ + book: booksResponse, + }); + }) + ); + } + + @Action(CreateUpdateBook) + save(ctx: StateContext, 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, action: DeleteBook) { + return this.bookService.deleteById(action.id); + } +} +``` + +- 我们导入了 `DeleteBook` . + +- 我们在文件末尾添加了 `DeleteBook` 动作监听器. + +#### 删除确认弹层 + +打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`. + +替换构造函数: + +```js +import { ConfirmationService } from '@abp/ng.theme.shared'; +//... + +constructor( + private store: Store, + private fb: FormBuilder, + private bookService: BookService, + private confirmation: ConfirmationService // <== added this line ==> +) { } +``` + +* 我们导入了 `ConfirmationService`. +* 我们在构造函数注入了 `ConfirmationService` . + +参阅[确认弹层文档](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)了解更多 + +在 `book-list.component.ts` 中添加删除方法: + +```js +import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/books.actions' ;// <== imported DeleteBook ==> + +import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==> + +//... + +delete(id: string) { + this.confirmation + .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') + .subscribe(status => { + if (status === Confirmation.Status.confirm) { + this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get()); + } + }); +} +``` + +`delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时分派动作. 确认弹层看起来如下: + +![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) + +#### 添加删除按钮 + +打开 `app\books\book-list` 文件夹下的 `app\books\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮: + +```html +
+ + +
+``` + +最终操作下拉框UI看起来如下: + +![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) + +{{end}} + +### 下一章 + +查看本教程的 [下一章](Part-3.md) . diff --git a/docs/zh-Hans/Tutorials/Part-3.md b/docs/zh-Hans/Tutorials/Part-3.md new file mode 100644 index 0000000000..cded954e6b --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-3.md @@ -0,0 +1,198 @@ +## ASP.NET Core {{UI_Value}} 教程 - 第三章 +````json +//[doc-params] +{ + "UI": ["MVC","NG"] +} +```` + +{{ +if UI == "MVC" + DB="ef" + DB_Text="Entity Framework Core" + UI_Text="mvc" +else if UI == "NG" + DB="mongodb" + DB_Text="MongoDB" + UI_Text="angular" +else + DB ="?" + UI_Text="?" +end +}} + +### 关于本教程 + +这是ASP.NET Core{{UI_Value}}系列教程的第二章. 共有三章: + +- [Part-1: 创建项目和书籍列表页面](Part-1.md) +- [Part 2: 创建,编辑,删除书籍](Part-2.md) +- **Part-3: 集成测试(本章)** + +> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). + +### 解决方案中的测试项目 + +解决方案中有多个测试项目: + +![bookstore-test-projects-v2](./images/bookstore-test-projects-{{UI_Text}}.png) + +每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: + +* [xunit](https://xunit.github.io/) 作为主测试框架. +* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. +* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. + +### 添加测试数据 + +启动模板包含`Acme.BookStore.TestBase`项目中的`BookStoreTestDataSeedContributor`类,它创建一些数据来运行测试. +更改`BookStoreTestDataSeedContributor`类如下所示: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; + +namespace Acme.BookStore +{ + public class BookStoreTestDataSeedContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public BookStoreTestDataSeedContributor( + IRepository bookRepository, + IGuidGenerator guidGenerator) + { + _bookRepository = bookRepository; + _guidGenerator = guidGenerator; + } + + public async Task SeedAsync(DataSeedContext context) + { + await _bookRepository.InsertAsync( + new Book(id: _guidGenerator.Create(), + name: "Test book 1", + type: BookType.Fantastic, + publishDate: new DateTime(2015, 05, 24), + price: 21 + ) + ); + + await _bookRepository.InsertAsync( + new Book(id: _guidGenerator.Create(), + name: "Test book 2", + type: BookType.Science, + publishDate: new DateTime(2014, 02, 11), + price: 15 + ) + ); + } + } +} +```` + +* 注入`IRepository`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. +* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../Guid-Generation.md)了解更多信息). + +### 测试 BookAppService + +在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: + +````csharp +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Validation; +using Microsoft.EntityFrameworkCore.Internal; + +namespace Acme.BookStore +{ + public class BookAppService_Tests : BookStoreApplicationTestBase + { + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "Test book 1"); + } + } +} +```` + +* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. + +新增测试方法,用以测试创建一个合法book实体的场景: + +````C# +[Fact] +public async Task Should_Create_A_Valid_Book() +{ + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "New test book 42", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); +} +```` + +新增测试方法,用以测试创建一个非法book实体失败的场景: + +````csharp +[Fact] +public async Task Should_Not_Create_A_Book_Without_Name() +{ + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + }); + + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); +} +```` + +* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. + +打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: + +![bookstore-appservice-tests](./images/bookstore-appservice-tests.png) + +恭喜, 绿色图标表示测试已成功通过! diff --git a/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png b/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png new file mode 100644 index 0000000000..e8243fedc7 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-add-create-dialog-v2.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png rename to docs/zh-Hans/Tutorials/images/bookstore-add-create-dialog-v2.png diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png b/docs/zh-Hans/Tutorials/images/bookstore-add-edit-dialog.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png rename to docs/zh-Hans/Tutorials/images/bookstore-add-edit-dialog.png diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-add-index-page-v2.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png rename to docs/zh-Hans/Tutorials/images/bookstore-add-index-page-v2.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png new file mode 100644 index 0000000000..a3197b6457 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png b/docs/zh-Hans/Tutorials/images/bookstore-appservice-tests.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png rename to docs/zh-Hans/Tutorials/images/bookstore-appservice-tests.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png new file mode 100644 index 0000000000..a460d4241b Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png new file mode 100644 index 0000000000..9e6cc9e010 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png b/docs/zh-Hans/Tutorials/images/bookstore-books-table-actions.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png rename to docs/zh-Hans/Tutorials/images/bookstore-books-table-actions.png diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png b/docs/zh-Hans/Tutorials/images/bookstore-books-table.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png rename to docs/zh-Hans/Tutorials/images/bookstore-books-table.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png b/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png new file mode 100644 index 0000000000..a80b180f1c Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png b/docs/zh-Hans/Tutorials/images/bookstore-create-dialog-2.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png rename to docs/zh-Hans/Tutorials/images/bookstore-create-dialog-2.png diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png b/docs/zh-Hans/Tutorials/images/bookstore-create-dialog.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png rename to docs/zh-Hans/Tutorials/images/bookstore-create-dialog.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png new file mode 100644 index 0000000000..b9eb38b8b7 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png new file mode 100644 index 0000000000..f453b20279 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png new file mode 100644 index 0000000000..6f19dcc7bf Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png new file mode 100644 index 0000000000..ec9ef4c42f Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png new file mode 100644 index 0000000000..857b10de5b Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png new file mode 100644 index 0000000000..8d78bd9a54 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-edit-button.png b/docs/zh-Hans/Tutorials/images/bookstore-edit-button.png new file mode 100644 index 0000000000..bfc1c64797 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-edit-button.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png b/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png new file mode 100644 index 0000000000..2a02802bb9 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png b/docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png new file mode 100644 index 0000000000..4f41829f0d Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png b/docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png new file mode 100644 index 0000000000..be7a919017 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png b/docs/zh-Hans/Tutorials/images/bookstore-homepage.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png rename to docs/zh-Hans/Tutorials/images/bookstore-homepage.png diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v2.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png rename to docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v2.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png b/docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png new file mode 100644 index 0000000000..591cffb121 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png b/docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png new file mode 100644 index 0000000000..629ad46444 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png new file mode 100644 index 0000000000..542cda209c Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png b/docs/zh-Hans/Tutorials/images/bookstore-menu-items.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png rename to docs/zh-Hans/Tutorials/images/bookstore-menu-items.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png new file mode 100644 index 0000000000..0724e4ae8f Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png new file mode 100644 index 0000000000..d59c0ce1d3 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png new file mode 100644 index 0000000000..8112fe1352 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png new file mode 100644 index 0000000000..72513de6e5 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png new file mode 100644 index 0000000000..95de64f8d4 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png b/docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png new file mode 100644 index 0000000000..97bf7fc7c1 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png b/docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png new file mode 100644 index 0000000000..a640eb2681 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png new file mode 100644 index 0000000000..2baea20236 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png b/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png rename to docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png b/docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png new file mode 100644 index 0000000000..cf6145e03f Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png new file mode 100644 index 0000000000..07d064a880 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png b/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-mvc.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png rename to docs/zh-Hans/Tutorials/images/bookstore-solution-structure-mvc.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png new file mode 100644 index 0000000000..08abf845a8 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png new file mode 100644 index 0000000000..133dc6f131 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png b/docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png new file mode 100644 index 0000000000..66d630bb56 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-swagger.png b/docs/zh-Hans/Tutorials/images/bookstore-swagger.png new file mode 100644 index 0000000000..3ce36a11bc Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-swagger.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png b/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist-network.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png rename to docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist-network.png diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png b/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png rename to docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png new file mode 100644 index 0000000000..6a8947238e Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-mvc.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png rename to docs/zh-Hans/Tutorials/images/bookstore-test-projects-mvc.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png new file mode 100644 index 0000000000..8701164d75 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png b/docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png new file mode 100644 index 0000000000..4889f4f757 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png differ diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png b/docs/zh-Hans/Tutorials/images/bookstore-user-management.png similarity index 100% rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png rename to docs/zh-Hans/Tutorials/images/bookstore-user-management.png diff --git a/docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png b/docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png new file mode 100644 index 0000000000..307e3516a5 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png differ diff --git a/docs/zh-Hans/Tutorials/images/generate-proxy-command.png b/docs/zh-Hans/Tutorials/images/generate-proxy-command.png new file mode 100644 index 0000000000..f850ce07a2 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/generate-proxy-command.png differ diff --git a/docs/zh-Hans/Tutorials/images/generated-proxies.png b/docs/zh-Hans/Tutorials/images/generated-proxies.png new file mode 100644 index 0000000000..9e466e7d55 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/generated-proxies.png differ diff --git a/docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png b/docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png new file mode 100644 index 0000000000..c9e2fc0e65 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png differ diff --git a/docs/zh-Hans/UI/Angular/Component-Replacement.md b/docs/zh-Hans/UI/Angular/Component-Replacement.md index e98ce4331e..dc7c1a33c4 100644 --- a/docs/zh-Hans/UI/Angular/Component-Replacement.md +++ b/docs/zh-Hans/UI/Angular/Component-Replacement.md @@ -2,7 +2,7 @@ 你可以将一些ABP的组件替换为你自己的自定义组件. -您可以**替换**但**不能自定义**默认ABP组件的原因是禁用或更改该组件的一部分可能会导致问题. 所以我们把这些组件称为可替换组件. +你可以**替换**但**不能自定义**默认ABP组件的原因是禁用或更改该组件的一部分可能会导致问题. 所以我们把这些组件称为可替换组件. ### 如何替换组件 diff --git a/docs/zh-Hans/UI/Angular/Config-State.md b/docs/zh-Hans/UI/Angular/Config-State.md index 94b522f3fc..de645c8968 100644 --- a/docs/zh-Hans/UI/Angular/Config-State.md +++ b/docs/zh-Hans/UI/Angular/Config-State.md @@ -164,7 +164,7 @@ const hasIdentityOrAccountPermission = this.config.getGrantedPolicy( ### 如何从Store中获取翻译 -`ConfigStateService` 的 `getLocalization` 用法翻译. 这里有一些示例: +`ConfigStateService` 的 `getLocalization` 方法用于翻译. 这里有一些示例: ```js // this.config is instance of ConfigStateService @@ -186,7 +186,7 @@ const defaultValue = this.config.getLocalization({ ## 分发方法 -`ConfigStateService` 有几种分发方法,让你方便地将预定义操作分发到 `Store`. +`ConfigStateService` 有几种分发方法,让你方便地将预定义操作分发到 `Store`. ### 如何从服务器获取应用程序配置 @@ -236,7 +236,7 @@ const newRoute: ABP.Route = { path: "page", invisible: false, order: 2, - requiredPolicy: "MyProjectName::MyNewPage" + requiredPolicy: "MyProjectName.MyNewPage" }; this.config.dispatchAddRoute(newRoute); @@ -245,19 +245,20 @@ this.config.dispatchAddRoute(newRoute); `newRoute` 将被放置在根级别,没有任何父路由,并且其url将存储为 `'/path'`. -如果你想要**添加一个子路由,您可以这样做:** +如果你想要**添加一个子路由,你可以这样做:** ```js +import { eIdentityRouteNames } from '@abp/ng.identity'; // this.config is instance of ConfigStateService const newRoute: ABP.Route = { - parentName: "AbpAccount::Login", + parentName: eIdentityRouteNames.IdentityManagement, name: "My New Page", iconClass: "fa fa-dashboard", path: "page", invisible: false, order: 2, - requiredPolicy: "MyProjectName::MyNewPage" + requiredPolicy: "MyProjectName.MyNewPage" }; this.config.dispatchAddRoute(newRoute); @@ -291,4 +292,4 @@ this.config.dispatchSetEnvironment({ ## 下一步是什么? -* [组件替换]](./Component-Replacement.md) \ No newline at end of file +- [修改菜单](./Modifying-the-Menu.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Confirmation-Service.md b/docs/zh-Hans/UI/Angular/Confirmation-Service.md new file mode 100644 index 0000000000..a1a181837c --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Confirmation-Service.md @@ -0,0 +1,181 @@ +# 确认弹层 + +你可以使用@abp/ng.theme.shared包中提供 `ConfirmationService` 放置在你项目的级别来显示确认弹层 + +## 入门 + +你不必在模块或组件级别提供 `ConfirmationService`,它已经在**根**级别提供,你可以在你的组件,指令或服务直接注入并使用它. + +```js +import { ConfirmationService } from '@abp/ng.theme.shared'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private confirmation: ConfirmationService) {} +} +``` + +## 用法 + +你可以使用 `ConfirmationService` 的 `success`, `warn`, `error` 和 `info` 方法显示一个确认弹层. + +### 如何显示一个确认弹层 + +```js +const confirmationStatus$ = this.confirmation.success('Message', 'Title'); +``` + +- `ConfirmationService` 方法接收三个参数,分别是 `message`, `title`, 和 `options`. +- `success`, `warn`, `error`, 和 `info` 方法返回一个[RxJS Subject](https://rxjs-dev.firebaseapp.com/guide/subject)监听确认弹层关闭事件. 事件值类型是 [`Confirmation.Status`](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts#L24)枚举. + +### 如何监听关闭事件 + +你可以订阅确认弹层关闭事件,例: + +```js +import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; + +constructor(private confirmation: ConfirmationService) {} + +this.confirmation + .warn('::WillBeDeleted', { key: '::AreYouSure', defaultValue: 'Are you sure?' }) + .subscribe((status: Confirmation.Status) => { + // your code here + }); +``` + +- `message` 和 `title` 参数接收字符串,本地化Key或本地化对象. 参阅[本地化文档](./Localization.md) +- `Confirmation.Status` 是一个枚举,具有三个属性; + - `Confirmation.Status.confirm` 是一个关闭事件值,当通过确认按钮关闭弹出窗口时触发此事件. + - `Confirmation.Status.reject` 是一个关闭事件值,当通过“取消”按钮关闭弹出窗口时触发此事件. + - `Confirmation.Status.dismiss` 是一个关闭事件值,当通过按Escape键关闭弹出窗口时触发此事件. + +如果你对确认状态不感兴趣,则不必订阅返回的observable: + +```js +this.confirmation.error('You are not authorized.', 'Error'); +``` + +### 如何显示具有给定选项的确认弹层 + +选项可以作为第三个参数传递给`success`, `warn`, `error`, 和 `info` 方法: + +```js +const options: Partial = { + hideCancelBtn: false, + hideYesBtn: false, + cancelText: 'Close', + yesText: 'Confirm', + messageLocalizationParams: ['Demo'], + titleLocalizationParams: [], +}; + +this.confirmation.warn( + 'AbpIdentity::RoleDeletionConfirmationMessage', + 'Are you sure?', + options, +); +``` + +- `hideCancelBtn` 选项为 `true` 时隐藏取消按钮. 默认值为 `false`. +- `hideYesBtn` 选项为 `true` 时隐藏确认按钮. 默认值为 `false`. +- `cancelText` 是取消按钮的文本,可以传递本地化键或本地化对象. 默认值是 `AbpUi::Cancel`. +- `yesText` 是确定按钮的文本,可以传递本地化键或本地化对象. 默认值是 `AbpUi::Yes`. +- `messageLocalizationParams`是用于消息本地化的插值参数. +- `titleLocalizationParams` 是标题本地化的插值参数. + +使用以上选项确认弹层窗口如下所示: + +![confirmation](./images/confirmation.png) + +你可以传递HTML字符串作为标题,消息或按钮文本. 例如: + +```js +const options: Partial = { + yesText: 'Yes, delete it', +}; + +this.confirmation.warn( + ` + Role Demo will be deleted +
+ Do you confirm that? + `, + 'Are you sure?', + options, +); +``` + +由于这些值现在是HTML,因此应该手动处理本地化. 参阅[LocalizationService](./Localization#using-the-localization-service)了解如何实现. + +> 注意,Angular会清除所有字符串,并且并非每个HTML字符串都可以使用. 仅显示被Angular视为"安全"的值. + +### 如何删除一个确认弹层 + +打开的确认弹出窗口可以通过 `clear` 方法手动删除: + +```js +this.confirmation.clear(); +``` + +## API + +### success + +```js +success( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +> 请参见[`Config.LocalizationParam`类型](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46)和[Confirmation名称空间](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts) + + +### warn + +```js +warn( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### error + +```js +error( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### info + +```js +info( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + options?: Partial, +): Observable +``` + +### clear + +```js +clear( + status: Confirmation.Status = Confirmation.Status.dismiss +): void +``` + +- `status` 参数是确认关闭事件的值. + + +## 下一步是什么? + +- [Toast Overlay](./Toaster-Service.md) diff --git a/docs/zh-Hans/UI/Angular/Content-Strategy.md b/docs/zh-Hans/UI/Angular/Content-Strategy.md index 3600f2dbf4..b4430f285d 100644 --- a/docs/zh-Hans/UI/Angular/Content-Strategy.md +++ b/docs/zh-Hans/UI/Angular/Content-Strategy.md @@ -1,6 +1,6 @@ # ContentStrategy -`ContentStrategy` 是@abp/ng.core包暴露出的抽象类. 它可以帮助您创建内联脚本或样式. +`ContentStrategy` 是@abp/ng.core包暴露出的抽象类. 它可以帮助你创建内联脚本或样式. ## API diff --git a/docs/zh-Hans/UI/Angular/Context-Strategy.md b/docs/zh-Hans/UI/Angular/Context-Strategy.md index 93abfa7cef..26d8ba9270 100644 --- a/docs/zh-Hans/UI/Angular/Context-Strategy.md +++ b/docs/zh-Hans/UI/Angular/Context-Strategy.md @@ -76,7 +76,7 @@ setContext(): undefined CONTEXT_STRATEGY.None() ``` -该策略不会将任何上下文传递到投影内容。 +该策略不会将任何上下文传递到投影内容. ### Component diff --git a/docs/zh-Hans/UI/Angular/Dom-Insertion-Service.md b/docs/zh-Hans/UI/Angular/Dom-Insertion-Service.md index 197ef9255c..e8d9420422 100644 --- a/docs/zh-Hans/UI/Angular/Dom-Insertion-Service.md +++ b/docs/zh-Hans/UI/Angular/Dom-Insertion-Service.md @@ -125,6 +125,16 @@ removeContent(element: HTMLScriptElement | HTMLStyleElement): void - `element` 参数是已插入的 `HTMLScriptElement` 或 `HTMLStyleElement` 元素,它们应由 `insertContent` 方法返回. +### has + +```js +has(content: string): boolean +``` + +`has` 返回一个布尔值,用于表示给定的内容是否插入到DOM. + +- `content` 参数是 `HTMLScriptElement` 或 `HTMLStyleElement` 元素的内容. + ## 下一步是什么? - [ContentProjectionService](./Content-Projection-Service.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md b/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md index abd08a0410..07753f8541 100644 --- a/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md +++ b/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md @@ -162,7 +162,7 @@ class DemoComponent { } ``` -在此示例中,第二个文件需要预先加载第一个文件, RxJS `concat` 函数将允许你以给定的顺序一个接一个地加载所有脚本,并且仅在加载所有脚本时放行. +在此示例中,第二个文件需要预先加载第一个文件, RxJS `concat` 函数将允许你以给定的顺序一个接一个地加载所有脚本,并且仅在加载所有脚本时放行. ## API diff --git a/docs/zh-Hans/UI/Angular/Loading-Strategy.md b/docs/zh-Hans/UI/Angular/Loading-Strategy.md new file mode 100644 index 0000000000..8e1142dd20 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Loading-Strategy.md @@ -0,0 +1,95 @@ +# LoadingStrategy + +`LoadingStrategy` 是@abp/ng.core包暴露的抽象类. 扩展它的有两种加载策略: `ScriptLoadingStrategy` 和 `StyleLoadingStrategy`. 它们实现相同的方法和属性,这两种策略都可以帮助你定义延迟加载的工作方式. + +## API + +### constructor + +```js +constructor( + public path: string, + protected domStrategy?: DomStrategy, + protected crossOriginStrategy?: CrossOriginStrategy +) +``` + +- `path` 将 `